PHP從零實(shí)現(xiàn)區(qū)塊鏈(四)交易1

前言

交易(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)。

UTXO模型

上圖中,所有的交易都可以找到前向交易,例如 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.phpTxOutput.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)在我們需要一種通用的普通交易,修改TransactionBlockChain

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è)試一下代碼:

測(cè)試結(jié)果

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容