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

wangwei_hz發表於2018-02-27

blockchain

最終內容請以原文為準:https://wangwei.one/posts/7890ab7e.html

引言

上一篇文章我們實現了區塊鏈的工作量證明機制(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(桶)"來儲存資料:

  • 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_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();
        }
    }

    /**
     * 儲存最新一個區塊的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 介面的程式碼邏輯,改為如下邏輯:

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

程式碼如下:

/**
  * <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://github.com/wangweiX/blockchain-java/tree/part3-persistence
  • https://jeiwan.cc/posts/building-blockchain-in-go-part-3/
  • 《精通比特幣》第二版

相關文章