前言
交易(transaction)是比特幣的核心所在,而區(qū)塊鏈的目的,也正是為了能夠安全可靠地存儲(chǔ)交易。在區(qū)塊鏈中,交易一旦被創(chuàng)建,就沒有任何人能夠再去修改或是刪除它。今天,我們將會(huì)開始實(shí)現(xiàn)交易,代碼變動(dòng)較大,這里查看
交易
一筆交易由一些輸入(input)和一些輸出(output)組合而來,下面是比特幣的UTXO 模型。UTXO 全稱是:“Unspent Transaction Output”,這指的是:未花費(fèi)的交易輸出。UTXO 的核心設(shè)計(jì)思路是無狀態(tài),它記錄的是交易事件,而不記錄最終狀態(tài),也就是說只記錄變更事件,用戶需要根據(jù)歷史記錄自行計(jì)算余額。對(duì)于比特幣而言,交易是通過一個(gè)腳本(script)來鎖定(lock)一些值(value),而這些值只可
以被鎖定它們的人解鎖(unlock)。

上圖中,所有的交易都可以找到前向交易,例如 TX5 的前向交易是 TX2,TX2 中的 Output1 作為 TX5 中的 Input0。那么有同學(xué)可能會(huì)問了,第一個(gè)交易的輸入對(duì)應(yīng)的是哪個(gè)交易的輸出呢?在比特幣中,第一筆交易只有輸出,沒有輸入。
實(shí)際上當(dāng)?shù)V工挖出一個(gè)新的塊時(shí),它會(huì)向新的塊中添加一個(gè) coinbase 交易。coinbase 交易是一種特殊的交易,它不需要引用之前一筆交易的輸出。它“憑空”產(chǎn)生了幣(也就是產(chǎn)生了新幣),這是礦工獲得挖出新塊的獎(jiǎng)勵(lì),也可以理解為“發(fā)行新幣”。
在區(qū)塊鏈的最初,也就是第一個(gè)塊,叫做創(chuàng)世塊。正是這個(gè)創(chuàng)世塊,產(chǎn)生了區(qū)塊鏈最開始的輸出。對(duì)于創(chuàng)世塊,不需要引用之前的交易輸出。因?yàn)樵趧?chuàng)世塊之前根本不存在交易,也就沒有不存在交易輸出。
下面我們來定義一個(gè)交易類 Transaction.php
<?php
namespace App\Services;
class Transaction
{
// coinbase 交易的獎(jiǎng)勵(lì)
const subsidy = 50;
/**
* 當(dāng)前交易的Hash
* @var string $id
*/
public $id;
/**
* @var TXInput[] $txInputs
*/
public $txInputs;
/**
* @var TXOutput[] $txOutputs
*/
public $txOutputs;
public function __construct(array $txInputs, array $txOutputs)
{
$this->txInputs = $txInputs;
$this->txOutputs = $txOutputs;
$this->setId();
}
private function setId()
{
$this->id = hash('sha256', serialize($this));
}
}
接著定義交易輸入與輸出,TxInput.php,TxOutput.php
<?php
namespace App\Services;
class TXInput
{
/**
* @var string $txId
*/
public $txId;
/**
* @var int $vOut
*/
public $vOut;
/**
* @var string $scriptSig
*/
public $scriptSig;
public function __construct(string $txId, int $vOut, string $scriptSig)
{
$this->txId = $txId;
$this->vOut = $vOut;
$this->scriptSig = $scriptSig;
}
public function canUnlockOutputWith(string $unlockingData): bool
{
return $this->scriptSig == $unlockingData;
}
}
<?php
namespace App\Services;
class TXOutput
{
/**
* @var int $value
*/
public $value;
/**
* @var string $scriptPubKey
*/
public $scriptPubKey;
public function __construct(int $value, string $scriptPubKey)
{
$this->value = $value;
$this->scriptPubKey = $scriptPubKey;
}
public function canBeUnlockedWith(string $unlockingData): bool
{
return $this->scriptPubKey == $unlockingData;
}
}
TXInput的txId就是該輸入引用的之前的某個(gè)交易的id,vOut指引用的該交易輸出的索引,也就是第幾個(gè)輸出,scriptSig是一個(gè)腳本,提供了可解鎖輸出結(jié)構(gòu)里面scriptPubKey字段的數(shù)據(jù)。如果scriptSig提供的數(shù)據(jù)是正確的,那么輸出就會(huì)被解鎖,然后被解鎖的值就可以被用于產(chǎn)生新的輸出;如果數(shù)據(jù)不正確,輸出就無法被引用在輸入中,或者說,無法使用這個(gè)輸出。這種機(jī)制,保證了用戶無法花費(fèi)屬于其他人的幣。
由于我們還沒有實(shí)現(xiàn)地址,所以目前scriptSig將僅僅存儲(chǔ)一個(gè)用戶自定義的任意錢包地址。我們會(huì)在下一篇文章中實(shí)現(xiàn)公鑰(public key)和簽名(signature)。所以暫時(shí)定義在輸入和輸出上的鎖定和解鎖方法: canUnlockOutputWith(),canBeUnlockedWith()。
存儲(chǔ)交易
之前我們?cè)?Block 里直接存儲(chǔ)的data,現(xiàn)在我們應(yīng)該替換為創(chuàng)建的交易。
<?php
namespace App\Services;
class Block
{
//......
/**
* @var Transaction[] $transactions
*/
public $transactions;
public function __construct(array $transactions, string $prevBlockHash)
{
$this->prevBlockHash = $prevBlockHash;
$this->transactions = $transactions;
$this->timestamp = time();
$pow = new ProofOfWork($this);
list($nonce, $hash) = $pow->run();
$this->nonce = $nonce;
$this->hash = $hash;
}
public static function NewGenesisBlock(Transaction $coinbase)
{
return $block = new Block([$coinbase], '');
}
public function hashTransactions(): string
{
$txsHashArr = [];
foreach ($this->transactions as $transaction) {
$txsHashArr[] = $transaction->id;
}
return hash('sha256', implode('', $txsHashArr));
}
}
相應(yīng)的需要修改 ProofOfWork 以適配加入的交易字段,$this->block->hashTransactions()使我們要計(jì)算的區(qū)塊Hash值包涵交易的摘要信息。
public function prepareData(int $nonce): string
{
return implode('', [
$this->block->prevBlockHash,
$this->block->hashTransactions(),
$this->block->timestamp,
config('blockchain.targetBits'),
$nonce
]);
}
現(xiàn)在我們創(chuàng)建創(chuàng)世區(qū)塊時(shí),需要傳入一個(gè) coinbase 交易,那我們?nèi)?shí)現(xiàn)創(chuàng)建 coinbase 交易的方法。在 Transaction 中加入 NewCoinbaseTX()
public static function NewCoinbaseTX(string $to, string $data): Transaction
{
if ($data == '') {
$data = sprintf("Reward to '%s'", $to);
}
$txIn = new TXInput('', -1, $data);
$txOut = new TXOutput(self::subsidy, $to);
return new Transaction([$txIn], [$txOut]);
}
入上面所說,coinbase 沒有輸入只有輸出,所以 $txIn = new TXInput('', -1, $data); 輸入結(jié)構(gòu)中我們將 $txId = '' $vOut=-1,沒有輸入也就不需要提供 scriptSig 去解鎖輸出,存?zhèn)€Reward to '%s'信息好啦。
發(fā)送幣
現(xiàn)在,我們想要給其他人發(fā)送一些幣。為此,我們需要?jiǎng)?chuàng)建一筆新的交易,將它放到一個(gè)塊里,然后挖出這個(gè)塊。之前我們只實(shí)現(xiàn)了 coinbase 交易(這是一種特殊的交易),現(xiàn)在我們需要一種通用的普通交易,修改Transaction和BlockChain
class Transaction
{
public static function NewUTXOTransaction(string $from, string $to, int $amount, BlockChain $bc): Transaction
{
list($acc, $validOutputs) = $bc->findSpendableOutputs($from, $amount);
if ($acc < $amount) {
echo "余額不足";
exit;
}
$inputs = [];
$outputs = [];
/**
* @var TXOutput $output
*/
foreach ($validOutputs as $txId => $outsIdx) {
foreach ($outsIdx as $outIdx) {
$inputs[] = new TXInput($txId, $outIdx, $from);
}
}
$outputs[] = new TXOutput($amount, $to);
if ($acc > $amount) {
$outputs[] = new TXOutput($acc - $amount, $from);
}
return new Transaction($inputs, $outputs);
}
public function isCoinbase(): bool
{
return (count($this->txInputs) == 1) && ($this->txInputs[0]->txId == '') && ($this->txInputs[0]->vOut == -1);
}
}
class BlockChain implements \Iterator
{
/**
* 找出地址的所有未花費(fèi)交易
* @param string $address
* @return Transaction[]
*/
public function findUnspentTransactions(string $address): array
{
$unspentTXs = [];
$spentTXOs = [];
/**
* @var Block $block
*/
foreach ($this as $block) {
foreach ($block->transactions as $tx) {
$txId = $tx->id;
foreach ($tx->txOutputs as $outIdx => $txOutput) {
if (isset($spentTXOs[$txId])) {
foreach ($spentTXOs[$txId] as $spentOutIdx) {
if ($spentOutIdx == $outIdx) {
continue 2;
}
}
}
if ($txOutput->canBeUnlockedWith($address)) {
$unspentTXs[$txId] = $tx;
}
}
if (!$tx->isCoinbase()) {
foreach ($tx->txInputs as $txInput) {
if ($txInput->canUnlockOutputWith($address)) {
$spentTXOs[$txInput->txId][] = $txInput->vOut;
}
}
}
}
}
return $unspentTXs;
}
/**
* 找出所有已花費(fèi)的輸出
* @param string $address
* @return array
*/
public function findSpentOutputs(string $address): array
{
$spentTXOs = [];
/**
* @var Block $block
*/
foreach ($this as $block) {
foreach ($block->transactions as $tx) {
if (!$tx->isCoinbase()) {
foreach ($tx->txInputs as $txInput) {
if ($txInput->canUnlockOutputWith($address)) {
$spentTXOs[$txInput->txId][] = $txInput->vOut;
}
}
}
}
}
return $spentTXOs;
}
// 根據(jù)所有未花費(fèi)的交易和已花費(fèi)的輸出,找出滿足金額的未花費(fèi)輸出,用于構(gòu)建交易
public function findSpendableOutputs(string $address, int $amount): array
{
$unspentOutputs = [];
$unspentTXs = $this->findUnspentTransactions($address);
$spentTXOs = $this->findSpentOutputs($address);
$accumulated = 0;
/**
* @var Transaction $tx
*/
foreach ($unspentTXs as $tx) {
$txId = $tx->id;
foreach ($tx->txOutputs as $outIdx => $txOutput) {
if (isset($spentTXOs[$txId])) {
foreach ($spentTXOs[$txId] as $spentOutIdx) {
if ($spentOutIdx == $outIdx) {
// 說明這個(gè)tx的這個(gè)outIdx被花費(fèi)過
continue 2;
}
}
}
if ($txOutput->canBeUnlockedWith($address) && $accumulated < $amount) {
$accumulated += $txOutput->value;
$unspentOutputs[$txId][] = $outIdx;
if ($accumulated >= $amount) {
break 2;
}
}
}
}
return [$accumulated, $unspentOutputs];
}
/**
* 找出所有未花費(fèi)的輸出
* @param string $address
* @return TXOutput[]
*/
public function findUTXO(string $address): array
{
$UTXOs = [];
$unspentTXs = $this->findUnspentTransactions($address);
$spentTXOs = $this->findSpentOutputs($address);
foreach ($unspentTXs as $transaction) {
$txId = $transaction->id;
foreach ($transaction->txOutputs as $outIdx => $output) {
if (isset($spentTXOs[$txId])) {
foreach ($spentTXOs[$txId] as $spentOutIdx) {
if ($spentOutIdx == $outIdx) {
// 說明這個(gè)tx的這個(gè)outIdx被花費(fèi)過
continue 2;
}
}
}
if ($output->canBeUnlockedWith($address)) {
$UTXOs[] = $output;
}
}
}
return $UTXOs;
}
}
這幾個(gè)方法代碼較長(zhǎng),邏輯其實(shí)不復(fù)雜。主要是遍歷區(qū)塊鏈的所有交易,找出未花費(fèi)的交易,已花費(fèi)的交易輸出,未花費(fèi)的交易輸出,來構(gòu)建一筆交易,以及后續(xù)查詢余額使用,多看看就理解了。
下面還需要移除 addBlock(),添加mineBlock(),以及修改NewBlockChain(),讓他們都支持創(chuàng)建交易。
/**
* @param array $transactions
* @throws \Exception
*/
public function mineBlock(array $transactions)
{
$lastHash = Cache::get('l');
if (is_null($lastHash)) {
echo "還沒有區(qū)塊鏈,請(qǐng)先初始化";
exit;
}
$block = new Block($transactions, $lastHash);
$this->tips = $block->hash;
Cache::put('l', $block->hash);
Cache::put($block->hash, serialize($block));
}
// 新建區(qū)塊鏈
public static function NewBlockChain(string $address): BlockChain
{
if (Cache::has('l')) {
// 存在區(qū)塊鏈
$tips = Cache::get('l');
} else {
$coinbase = Transaction::NewCoinbaseTX($address, self::genesisCoinbaseData);
$genesis = Block::NewGenesisBlock($coinbase);
Cache::put($genesis->hash, serialize($genesis));
Cache::put('l', $genesis->hash);
$tips = $genesis->hash;
}
return new BlockChain($tips);
}
OK,至此我們的區(qū)塊鏈基本支持交易了,下面還需要更新CLI的交互。新建 Send 命令與 Balance 命令。
class Balance extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'getbalance {address}';
/**
* The console command description.
*
* @var string
*/
protected $description = '查詢給定地址余額';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$address = $this->argument('address');
$bc = BlockChain::GetBlockChain();
$UTXOs = $bc->findUTXO($address);
$balance = 0;
foreach ($UTXOs as $output) {
$balance += $output->value;
}
$this->info(sprintf("balance of address '%s' is: %s", $address, $balance));
}
}
class Send extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'send {from : 發(fā)送地址} {to : 接收地址} {amount : 發(fā)送金額}';
/**
* The console command description.
*
* @var string
*/
protected $description = '發(fā)送比特幣給某人';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$arguments = $this->arguments();
$from = $arguments['from'];
$to = $arguments['to'];
$amount = $arguments['amount'];
$bc = BlockChain::GetBlockChain();
$tx = Transaction::NewUTXOTransaction($from, $to, $amount, $bc);
$bc->mineBlock([$tx]);
$this->info('send success');
foreach ($bc as $block) {
$this->info("{$block->hash}");
break;
}
}
}
不容易啊,下面來測(cè)試一下代碼:

看起來沒啥問題,搞定!
最后提示一下,沒有貼完所有修改的代碼,具體這里查看