到目前為止,我們已經構建了一個有工作量證明機制的區塊鏈。在這篇文章中,我們會將區塊鏈持久化,而不是隻在記憶體中,然後會提供一個簡單的命令列介面,用來完成一些與區塊鏈的互動操作,程式碼變動較大,點選這裡檢視
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,他的作用是:
- 如果資料庫已存在
l
,也就是有最後一個塊的雜湊,那就取出並建立一個 Blockchain 例項; - 如果不存在
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);
}
}
}
下面來測試一下:
在根目錄下,我們也能看到多出來了快取使用的storage目錄,裡面就是持久化的區塊鏈資料。
下一節,我們會一起實現交易,加油。
本作品採用《CC 協議》,轉載必須註明作者和本文連結