LaravelZero 从零实现区块链(四)交易 1

前言

交易(transaction)是比特币的核心所在,而区块链的目的,也正是为了能够安全可靠地存储交易。在区块链中,交易一旦被创建,就没有任何人能够再去修改或是删除它。今天,我们将会开始实现交易,代码变动较大,这里查看

交易

一笔交易由一些输入(input)和一些输出(output)组合而来,下面是比特币的UTXO 模型。UTXO 全称是:“Unspent Transaction Output”,这指的是:未花费的交易输出。

UTXO 的核心设计思路是无状态,它记录的是交易事件,而不记录最终状态,也就是说只记录变更事件,用户需要根据历史记录自行计算余额。对于比特币而言,交易是通过一个脚本(script)来锁定(lock)一些值(value),而这些值只可
以被锁定它们的人解锁(unlock)。

UTXO模型

上图中,所有的交易都可以找到前向交易,例如 TX5 的前向交易是 TX2,TX2 中的 Output1 作为 TX5 中的 Input0。那么有同学可能会问了,第一个交易的输入对应的是哪个交易的输出呢?在比特币中,第一笔交易只有输出,没有输入。

实际上当矿工挖出一个新的块时,它会向新的块中添加一个 coinbase 交易。coinbase 交易是一种特殊的交易,它不需要引用之前一笔交易的输出。它“凭空”产生了币,这是矿工获得挖出新块的奖励,也可以理解为“发行新币”。

在区块链的最初,也就是第一个块,叫做创世块。正是这个创世块,产生了区块链最开始的输出。对于创世块,不需要引用之前的交易输出。因为在创世块之前根本不存在交易,也就不存在交易输出。

下面我们来定义一个交易类 Transaction.php

<?php
namespace App\Services;

class Transaction
{
    // coinbase 交易的奖励
    const subsidy = 50;

    /**
     * 当前交易的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就是该输入引用的之前的某个交易的idvOut指引用的该交易输出的索引,也就是第几个输出,scriptSig是一个脚本,提供了可解锁输出结构里面scriptPubKey字段的数据。

如果scriptSig提供的数据是正确的,那么输出就会被解锁,然后被解锁的值就可以被用于产生新的输出;如果数据不正确,输出就无法被引用在输入中,或者说,无法使用这个输出。这种机制,保证了用户无法花费属于其他人的币。

由于我们还没有实现地址,所以目前scriptSig将仅仅存储一个用户自定义的任意钱包地址。我们会在下一篇文章中实现公钥(public key)和签名(signature)。所以暂时定义在输入和输出上的锁定和解锁方法: canUnlockOutputWith()canBeUnlockedWith()

存储交易

之前我们在 Block 里直接存储的data,现在我们应该替换为创建的交易。

<?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));
    }
}

相应的需要修改 ProofOfWork 以适配加入的交易字段,$this->block->hashTransactions()使我们要计算的区块Hash值包涵交易的摘要信息。

    public function prepareData(int $nonce): string
    {
        return implode('', [
            $this->block->prevBlockHash,
            $this->block->hashTransactions(),
            $this->block->timestamp,
            config('blockchain.targetBits'),
            $nonce
        ]);
    }

现在我们创建创世区块时,需要传入一个 coinbase 交易,那我们去实现创建 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); 输入结构中我们将 $txId = '' $vOut=-1,没有输入也就不需要提供 scriptSig 去解锁输出,存个Reward to '%s'信息好啦。

发送币

现在,我们想要给其他人发送一些币。为此,我们需要创建一笔新的交易,将它放到一个块里,然后挖出这个块。之前我们只实现了 coinbase 交易(这是一种特殊的交易),现在我们需要一种通用的普通交易,修改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
{
/**
     * 找出地址的所有未花费交易
     * @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;
    }

    /**
     * 找出所有已花费的输出
     * @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;
    }

    // 根据所有未花费的交易和已花费的输出,找出满足金额的未花费输出,用于构建交易
    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) {
                            // 说明这个tx的这个outIdx被花费过
                            continue 2;
                        }
                    }
                }

                if ($txOutput->canBeUnlockedWith($address) && $accumulated < $amount) {
                    $accumulated += $txOutput->value;
                    $unspentOutputs[$txId][] = $outIdx;
                    if ($accumulated >= $amount) {
                        break 2;
                    }
                }
            }
        }
        return [$accumulated, $unspentOutputs];
    }

    /**
     * 找出所有未花费的输出
     * @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) {
                            // 说明这个tx的这个outIdx被花费过
                            continue 2;
                        }
                    }
                }

                if ($output->canBeUnlockedWith($address)) {
                    $UTXOs[] = $output;
                }
            }
        }
        return $UTXOs;
    }
}

这几个方法代码较长,逻辑其实不复杂,多看看就理解了。

findUnspentTransactions($address)倒序遍历区块链中的所有交易,找出对应地址的所有未花费交易;findSpentOutputs($address)找出了地址所有的已花费输出,该返回值里包含了交易的ID,和对应的已花费输出的索引。

findSpendableOutputs($address, $amount)根据前两个方法返回的信息,构造出满足amount条件的未花费输出结构,我们会使用这些未花费输出去创建一个新的UTXO。findUTXO($address)返回地址的所有的未花费输出,为后续查询余额使用

下面还需要移除 addBlock(),添加mineBlock(),以及修改NewBlockChain(),让他们都支持创建交易。

    /**
     * @param array $transactions
     * @throws \Exception
     */
    public function mineBlock(array $transactions)
    {
        $lastHash = Cache::get('l');
        if (is_null($lastHash)) {
            echo "还没有区块链,请先初始化";
            exit;
        }

        $block = new Block($transactions, $lastHash);

        $this->tips = $block->hash;
        Cache::put('l', $block->hash);
        Cache::put($block->hash, serialize($block));
    }

    // 新建区块链
    public static function NewBlockChain(string $address): BlockChain
    {
        if (Cache::has('l')) {
            // 存在区块链
            $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,至此我们的区块链基本支持交易了,下面还需要更新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 : 发送地址} {to : 接收地址} {amount : 发送金额}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '发送比特币给某人';

    /**
     * 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;
        }
    }
}

不容易啊,下面来测试一下代码:

测试结果

看起来没啥问题,搞定!
最后提示一下,没有贴完所有修改的代码,具体这里查看

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!