LaravelZero 從零實現區塊鏈(三)資料持久化與 CLI

鼓樓的夜色中發表於2020-05-02

到目前為止,我們已經構建了一個有工作量證明機制的區塊鏈。在這篇文章中,我們會將區塊鏈持久化,而不是隻在記憶體中,然後會提供一個簡單的命令列介面,用來完成一些與區塊鏈的互動操作,程式碼變動較大,點選這裡檢視

Bitcoin Core ,最初由中本聰釋出,現在是比特幣的一個參考實現,它使用的是 LevelDB。我們為了方便,使用 Laravel提供的基於檔案的快取,大家也可以換成其他 K-V 資料庫。

在 config 目錄下新建一個 cache.php 配置檔案。

<?php
return [
    'default' => 'file',
    'stores' => [
        'file' => [
            'driver' => 'file',
            'path' => storage_path(),
        ],
    ],
    'prefix' => 'bc_'
];

在開始實現持久化的邏輯之前,我們首先需要決定到底要如何在資料庫中進行儲存。為此,我們可以參考 Bitcoin Core 的做法:

簡單來說,Bitcoin Core 使用兩個 “bucket” 來儲存資料:
其中一個 bucket 是 blocks,它儲存了描述一條鏈中所有塊的後設資料。
另一個 bucket 是 chainstate,儲存了一條鏈的狀態,也就是當前所有的未花費的交易輸出,和一些後設資料。
此外,出於效能的考慮,Bitcoin Core 將每個區塊(block)儲存為磁碟上的不同檔案。如此一來,就不需要僅僅為了讀取一個單一的塊而將所有(或者部分)的塊都載入到記憶體中。但是,為了簡單起見,我們並不會實現這一點。

在 blocks 中,key -> value 為:

key value
b + 32 位元組的 block hash block index record
f + 4 位元組的 file number 檔案編號
l + 4 位元組的 file number 最後一個塊記錄
R + 1 位元組的 boolean 是否正在重新索引
F + 1 位元組的 flag name length + flag name string 1 byte boolean: various flags that can be on or off
t + 32 位元組的 transaction hash transaction index record

在 chainstate,key -> value 為:

key value
c + 32 位元組的 transaction hash unspent transaction output record for that transaction
B 32 位元組的 block hash: the block hash up to which the database represents the unspent transaction outputs

因為目前還沒有交易,所以我們只需要 blocks bucket。
另外,正如上面提到的,我們會將整個資料庫儲存為單個檔案,而不是將區塊儲存在不同的檔案中。所以,我們也不會需要檔案編號(file number)相關的東西。最終,我們會用到的鍵值對有:

32 位元組的 block-hash -> block 結構
l -> 鏈中最後一個塊的 hash
這就是實現持久化機制所有需要了解的內容了。

下面修改 BlockChain.php 以及 Block.php

class BlockChain
{
    /**
     * // 存放最後一個塊的hash
     * @var string $tips
     */
    public $tips;

    public function __construct(string $tips)
    {
        $this->tips = $tips;
    }

    // 加入一個塊到區塊鏈中
    public function addBlock(string $data)
    {
        // 獲取最後一個塊
        $prevBlock = unserialize(Cache::get($this->tips));

        $newBlock = new Block($data, $prevBlock->hash);

        // 存入最後一個塊到資料庫,並更新 l 和 tips
        Cache::put($newBlock->hash, serialize($newBlock));
        Cache::put('l', $newBlock->hash);
        $this->tips = $newBlock->hash;
    }

    // 新建區塊鏈
    public static function NewBlockChain(): BlockChain
    {
        if (Cache::has('l')) {
            // 存在區塊鏈
            $tips = Cache::get('l');
        } else {
            $genesis = Block::NewGenesisBlock();

            Cache::put($genesis->hash, serialize($genesis));

            Cache::put('l', $genesis->hash);

            $tips = $genesis->hash;
        }
        return new BlockChain($tips);
    }
}


class Block
{
    public static function NewGenesisBlock()
    {
        return $block = new Block('Genesis Block', '');
    }
}

現在我們的 BlockChain 不再需要 $blocks 這個陣列變數,而是使用一個 $tips,存放最後一個區塊的Hash。
然後移除原來的 NewGenesisBlock 方法,新建一個 NewBlockChain,他的作用是:

  1. 如果資料庫已存在l,也就是有最後一個塊的雜湊,那就取出並建立一個 Blockchain 例項;
  2. 如果不存在l,就建立一個創世區塊,並更新資料庫,在建立出 Blockchain 例項。

注意我們是將 Block 序列化 serialize() 以後才存入資料庫的,再取出時需要進行反序列化 unserialize()

最後修改 addBlock 方法,適配持久化邏輯。

現在我們的區塊鏈資料存放於資料庫中,不方便列印區塊資訊了。現在我們來解決這個問題。
PHP提供了迭代器介面,讓我們能自己實現遍歷邏輯,修改 BlockChain.php

// 實現迭代器介面
class BlockChain implements \Iterator
{
    // ......

    /**
     * 迭代器指向的當前塊Hash
     * @var string $iteratorHash
     */
    private $iteratorHash;

    /**
     * 迭代器指向的當前塊Hash
     * @var Block $iteratorBlock
     */
    private $iteratorBlock;

    /**
     * @inheritDoc
     */
    public function current()
    {
        return $this->iteratorBlock = unserialize(Cache::get($this->iteratorHash));
    }

    /**
     * @inheritDoc
     */
    public function next()
    {
        return $this->iteratorHash = $this->iteratorBlock->prevBlockHash;
    }

    /**
     * @inheritDoc
     */
    public function key()
    {
        return $this->iteratorHash;
    }

    /**
     * @inheritDoc
     */
    public function valid()
    {
        return $this->iteratorHash != '';
    }

    /**
     * @inheritDoc
     */
    public function rewind()
    {
        $this->iteratorHash = $this->tips;
    }
}

新增兩個成員變數,$iteratorHash 記錄當前遍歷的Hash,$iteratorBlock 是當前區塊;然後實現迭代器方法。

下面我們來實現命令列操作,讓我們能使用命令列與程式進行互動!

在專案根目錄下使用下面的命令

$ php blockchain make:command InitBlockChain
$ php blockchain make:command PrintChain
$ php blockchain make:command AddBlock

你的專案名也許和我的(blockchain)不一樣,可以使用
php application app:rename blockchain 來修改專案名。
完成命令建立後可以在 app/Commands 下找到建立的檔案。

class InitBlockChain extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'init-blockchain';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '初始化一個區塊鏈,如果沒有則建立';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $this->task('init blockchain', function () {
            BlockChain::NewBlockChain();
            return true;
        });
    }
}

class AddBlock extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'addblock {data : 區塊記錄的資料}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '向區塊鏈中新增一個區塊';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $data = $this->argument('data');
        $this->task('mining block:', function () use ($data) {
            $bc = BlockChain::NewBlockChain();
            $bc->addBlock($data);
            return true;
        });
    }
}


class PrintChain extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'printchain';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '格式化列印出所有塊資訊';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $bc = BlockChain::NewBlockChain();
        foreach ($bc as $block) {
            $this->info('-----------------');
            $this->info('   hash: ' . $block->hash);
            $this->info('   prev hash: ' . $block->prevBlockHash);
            $this->info('   timestamp: ' . $block->timestamp);
            $this->info('   data: ' . $block->data);
        }
    }
}

下面來測試一下:
cli-test.png

在根目錄下,我們也能看到多出來了快取使用的storage目錄,裡面就是持久化的區塊鏈資料。

下一節,我們會一起實現交易,加油。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章