1. 程式人生 > >基於Java語言構建區塊鏈(三)—— 持久化 & 命令行

基於Java語言構建區塊鏈(三)—— 持久化 & 命令行

java 分布式 編程語言 程序員

技術分享圖片
引言
上一篇 文章我們實現了區塊鏈的工作量證明機制(Pow),盡可能地實現了挖礦。但是距離真正的區塊鏈應用還有很多重要的特性沒有實現。今天我們來實現區塊鏈數據的存儲機制,將每次生成的區塊鏈數據保存下來。有一點需要註意,區塊鏈本質上是一款分布式的數據庫,我們這裏不實現"分布式",只聚焦於數據存儲部分。

給大家推薦一個java內部學習群:725633148,進群找管理免費領取學習資料和視頻。沒有錯就是免費領取!大佬小白都歡迎,大家一起學習共同進步!

數據庫選擇

到目前為止,我們的實現機制中還沒有區塊存儲這一環節,導致我們的區塊每次生成之後都保存在了內存中。這樣不便於我們重新使用區塊鏈,每次都要從頭開始生成區塊,也不能夠跟他人共享我們的區塊鏈,因此,我們需要將其存儲在磁盤上。

我們該選擇哪一款數據庫呢?事實上,在《比特幣白皮書》中並沒有明確指定使用哪一種的數據庫,因此這個由開發人員自己決定。中本聰 開發的 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;

/**

  • 序列化工具類
  • @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_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 {

@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
給大家推薦一個java內部學習群:725633148,進群找管理免費領取學習資料和視頻。沒有錯就是免費領取!大佬小白都歡迎,大家一起學習共同進步!

總結
本篇我們實現了區塊鏈的存儲功能,接下來我們將實現地址、交易、錢包這一些列的功能。

基於Java語言構建區塊鏈(三)—— 持久化 & 命令行