摘要:我們該選擇哪一款數(shù)據(jù)庫呢事實上,在比特幣白皮書中并沒有明確指定使用哪一種的數(shù)據(jù)庫,因此這個由開發(fā)人員自己決定。詳見精通比特幣第二版第章節(jié)交易的輸入與輸出此外,每個區(qū)塊數(shù)據(jù)都是以多帶帶的文件形式存儲在磁盤上。資料源代碼精通比特幣第二版
最終內(nèi)容請以原文為準:https://wangwei.one/posts/35c...引言
上一篇 文章我們實現(xiàn)了區(qū)塊鏈的工作量證明機制(Pow),盡可能地實現(xiàn)了挖礦。但是距離真正的區(qū)塊鏈應用還有很多重要的特性沒有實現(xiàn)。今天我們來實現(xiàn)區(qū)塊鏈數(shù)據(jù)的存儲機制,將每次生成的區(qū)塊鏈數(shù)據(jù)保存下來。有一點需要注意,區(qū)塊鏈本質(zhì)上是一款分布式的數(shù)據(jù)庫,我們這里不實現(xiàn)"分布式",只聚焦于數(shù)據(jù)存儲部分。
數(shù)據(jù)庫選擇到目前為止,我們的實現(xiàn)機制中還沒有區(qū)塊存儲這一環(huán)節(jié),導致我們的區(qū)塊每次生成之后都保存在了內(nèi)存中。這樣不便于我們重新使用區(qū)塊鏈,每次都要從頭開始生成區(qū)塊,也不能夠跟他人共享我們的區(qū)塊鏈,因此,我們需要將其存儲在磁盤上。
我們該選擇哪一款數(shù)據(jù)庫呢?事實上,在《比特幣白皮書》中并沒有明確指定使用哪一種的數(shù)據(jù)庫,因此這個由開發(fā)人員自己決定。中本聰 開發(fā)的 Bitcoin Core 中使用的是LevelDB。原文 Building Blockchain in Go. Part 3: Persistence and CLI 中使用的是 BoltDB ,對Go語言支持比較好。
但是我們這里使用的是Java來實現(xiàn),BoltDB不支持Java,這里我們選用 Rocksdb
RocksDB是由Facebook數(shù)據(jù)庫工程團隊開發(fā)和維護的一款key-value存儲引擎,比LevelDB性能更加強大,有關(guān)Rocksdb的詳細介紹,請移步至官方文檔:https://github.com/facebook/r... ,這里不多做介紹。數(shù)據(jù)結(jié)構(gòu)
在我們開始實現(xiàn)數(shù)據(jù)持久化之前,我們先要確定我們該如何去存儲我們的數(shù)據(jù)。為此,我們先來看看比特幣是怎么做的。
簡單來講,比特幣使用了兩個"buckets(桶)"來存儲數(shù)據(jù):
blocks. 描述鏈上所有區(qū)塊的元數(shù)據(jù).
chainstate. 存儲區(qū)塊鏈的狀態(tài),指的是當前所有的UTXO(未花費交易輸出)以及一些元數(shù)據(jù).
“在比特幣的世界里既沒有賬戶,也沒有余額,只有分散到區(qū)塊鏈里的UTXO。”詳見:《精通比特幣》第二版 第06章節(jié) —— 交易的輸入與輸出
此外,每個區(qū)塊數(shù)據(jù)都是以多帶帶的文件形式存儲在磁盤上。這樣做是出于性能的考慮:當讀取某一個多帶帶的區(qū)塊數(shù)據(jù)時,不需要加載所有的區(qū)塊數(shù)據(jù)到內(nèi)存中來。
在 blocks 這個桶中,存儲的鍵值對:
"b" + 32-byte block hash -> block index record
區(qū)塊的索引記錄
"f" + 4-byte file number -> file information record
文件信息記錄
"l" -> 4-byte file number: the last block file number used
最新的一個區(qū)塊所使用的文件編碼
"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
各種可以打開或關(guān)閉的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
數(shù)據(jù)庫所表示的UTXO的區(qū)塊Hash(抱歉,這一點我還沒弄明白……)
由于我們還沒有實現(xiàn)交易相關(guān)的特性,因此,我們這里只使用 block 桶。另外,前面提到過的,這里我們不會實現(xiàn)各個區(qū)塊數(shù)據(jù)各自存儲在獨立的文件上,而是統(tǒng)一存放在一個文件里面。因此,我們不要存儲和文件編碼相關(guān)的數(shù)據(jù),這樣一來,我們所用到的鍵值對就簡化為:
32-byte block-hash -> Block structure (serialized)
區(qū)塊數(shù)據(jù)與區(qū)塊hash的鍵值對
"l" -> the hash of the last block in a chain
最新一個區(qū)塊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 對象對應的字節(jié)數(shù)組 * @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; } }持久化
上面已經(jīng)說過,我們這里使用RocksDB,我們先寫一個相關(guān)的工具類RocksDBUtils,主要的功能如下:
putLastBlockHash:保存最新一個區(qū)塊的Hash值
getLastBlockHash:查詢最新一個區(qū)塊的Hash值
putBlock:保存區(qū)塊
getBlock:查詢區(qū)塊
注意:BoltDB 支持 Bucket 的特性,而RocksDB 不支持,我們這里采用統(tǒng)一前綴的方式進行處理。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 { /** * 區(qū)塊鏈數(shù)據(jù)文件 */ private static final String DB_FILE = "blockchain.db"; /** * 區(qū)塊桶前綴 */ 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(); } } /** * 保存最新一個區(qū)塊的Hash值 * * @param tipBlockHash */ public void putLastBlockHash(String tipBlockHash) throws Exception { rocksDB.put(SerializeUtils.serialize(BLOCKS_BUCKET_PREFIX + "l"), SerializeUtils.serialize(tipBlockHash)); } /** * 查詢最新一個區(qū)塊的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 ""; } /** * 保存區(qū)塊 * * @param block */ public void putBlock(Block block) throws Exception { byte[] key = SerializeUtils.serialize(BLOCKS_BUCKET_PREFIX + block.getHash()); rocksDB.put(key, SerializeUtils.serialize(block)); } /** * 查詢區(qū)塊 * * @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)); } }創(chuàng)建區(qū)塊鏈
現(xiàn)在我們來優(yōu)化 Blockchain.newBlockchain 接口的代碼邏輯,改為如下邏輯:
代碼如下:
/** *創(chuàng)建區(qū)塊鏈
* * @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 的數(shù)據(jù)結(jié)構(gòu),只記錄最新一個區(qū)塊鏈的Hash值
public class Blockchain { @Getter private String lastBlockHash; private Blockchain(String lastBlockHash) { this.lastBlockHash = lastBlockHash; } }
每次挖礦完成后,我們也需要將最新的區(qū)塊信息保存下來,并且更新最新區(qū)塊鏈Hash值:
/** *添加區(qū)塊
* * @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)); } /** *添加區(qū)塊
* * @param block */ public void addBlock(Block block) throws Exception { RocksDBUtils.getInstance().putLastBlockHash(block.getHash()); RocksDBUtils.getInstance().putBlock(block); this.lastBlockHash = block.getHash(); }
到此,存儲部分的功能就實現(xiàn)完畢,我們還缺少一個功能:
檢索區(qū)塊鏈現(xiàn)在,我們所有的區(qū)塊都保存到了數(shù)據(jù)庫,因此,我們能夠重新打開已有的區(qū)塊鏈并且向其添加新的區(qū)塊。但這也導致我們再也無法打印出區(qū)塊鏈中所有區(qū)塊的信息,因為,我們沒有將區(qū)塊存儲在數(shù)組當中。讓我們來修復這個瑕疵!
我們在Blockchain中創(chuàng)建一個內(nèi)部類 BlockchainIterator ,作為區(qū)塊鏈的迭代器,通過區(qū)塊之前的hash連接來依次迭代輸出區(qū)塊信息,代碼如下:
public class Blockchain { .... /** * 區(qū)塊鏈迭代器 */ public class BlockchainIterator { private String currentBlockHash; public BlockchainIterator(String currentBlockHash) { this.currentBlockHash = currentBlockHash; } /** * 是否有下一個區(qū)塊 * * @return */ public boolean hashNext() throws Exception { if (StringUtils.isBlank(currentBlockHash)) { return false; } Block lastBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash); if (lastBlock == null) { return false; } // 創(chuàng)世區(qū)塊直接放行 if (lastBlock.getPrevBlockHash().length() == 0) { return true; } return RocksDBUtils.getInstance().getBlock(lastBlock.getPrevBlockHash()) != null; } /** * 返回區(qū)塊 * * @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 部分的內(nèi)容,這里不做詳細介紹,具體可以去查看文末的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 # 添加區(qū)塊 $ 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" # 打印區(qū)塊鏈 $ java -jar blockchain-java-jar-with-dependencies.jar -print總結(jié)
本篇我們實現(xiàn)了區(qū)塊鏈的存儲功能,接下來我們將實現(xiàn)地址、交易、錢包這一些列的功能。
資料源代碼:https://github.com/wangweiX/b...
https://jeiwan.cc/posts/build...
《精通比特幣》第二版
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/24018.html
摘要:我們該選擇哪一款數(shù)據(jù)庫呢事實上,在比特幣白皮書中并沒有明確指定使用哪一種的數(shù)據(jù)庫,因此這個由開發(fā)人員自己決定。詳見精通比特幣第二版第章節(jié)交易的輸入與輸出此外,每個區(qū)塊數(shù)據(jù)都是以單獨的文件形式存儲在磁盤上。資料源代碼精通比特幣第二版 showImg(https://segmentfault.com/img/remote/1460000013923488?w=1200&h=627); 最...
摘要:截止年月號,比特幣中有個區(qū)塊,并且這些數(shù)據(jù)占據(jù)了的磁盤空間。每個比特幣節(jié)點都是路由區(qū)塊鏈數(shù)據(jù)庫挖礦錢包服務的功能集合。是比特幣的輕量級節(jié)點,它不需要下載所有的區(qū)塊鏈數(shù)據(jù),也不需要驗證區(qū)塊和交易數(shù)據(jù)。 showImg(https://img.i7years.com/blog/pexels-photo-38136.jpeg); 最終內(nèi)容請以原文為準:https://wangwei.one/...
摘要:截止年月號,比特幣中有個區(qū)塊,并且這些數(shù)據(jù)占據(jù)了的磁盤空間。每個比特幣節(jié)點都是路由區(qū)塊鏈數(shù)據(jù)庫挖礦錢包服務的功能集合。是比特幣的輕量級節(jié)點,它不需要下載所有的區(qū)塊鏈數(shù)據(jù),也不需要驗證區(qū)塊和交易數(shù)據(jù)。 showImg(https://img.i7years.com/blog/pexels-photo-38136.jpeg); 最終內(nèi)容請以原文為準:https://wangwei.one/...
摘要:第一節(jié)課程概述本課程面向初學者,內(nèi)容涵蓋以太坊開發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個基于以太坊的完整去中心化應用區(qū)塊鏈投票系統(tǒng)。第七節(jié)以太坊世界計算機以太坊是一種區(qū)塊鏈的實現(xiàn)。交易數(shù)據(jù)以太坊中每筆交易都存儲在區(qū)塊鏈上。 第一節(jié) 課程概述 本課程面向初學者,內(nèi)容涵蓋以太坊開發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個 基于以太坊的完整去中心化應用 —— 區(qū)塊鏈投票系統(tǒng)。 ...
摘要:第一節(jié)課程概述本課程面向初學者,內(nèi)容涵蓋以太坊開發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個基于以太坊的完整去中心化應用區(qū)塊鏈投票系統(tǒng)。第七節(jié)以太坊世界計算機以太坊是一種區(qū)塊鏈的實現(xiàn)。交易數(shù)據(jù)以太坊中每筆交易都存儲在區(qū)塊鏈上。 第一節(jié) 課程概述 本課程面向初學者,內(nèi)容涵蓋以太坊開發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個 基于以太坊的完整去中心化應用 —— 區(qū)塊鏈投票系統(tǒng)。 ...
閱讀 3136·2021-11-15 18:14
閱讀 1790·2021-09-22 10:51
閱讀 3306·2021-09-09 09:34
閱讀 3519·2021-09-06 15:02
閱讀 1038·2021-09-01 11:40
閱讀 3199·2019-08-30 13:58
閱讀 2537·2019-08-30 11:04
閱讀 1092·2019-08-28 18:31