LaravelZero 从零实现区块链(四)交易 1
前言
交易(transaction)是比特币的核心所在,而区块链的目的,也正是为了能够安全可靠地存储交易。在区块链中,交易一旦被创建,就没有任何人能够再去修改或是删除它。今天,我们将会开始实现交易,代码变动较大,这里查看
交易
一笔交易由一些输入(input)和一些输出(output)组合而来,下面是比特币的UTXO 模型。UTXO 全称是:“Unspent Transaction Output”,这指的是:未花费的交易输出。
UTXO 的核心设计思路是无状态,它记录的是交易事件,而不记录最终状态,也就是说只记录变更事件,用户需要根据历史记录自行计算余额。对于比特币而言,交易是通过一个脚本(script)来锁定(lock)一些值(value),而这些值只可
以被锁定它们的人解锁(unlock)。
上图中,所有的交易都可以找到前向交易,例如 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.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
就是该输入引用的之前的某个交易的id
,vOut
指引用的该交易输出的索引,也就是第几个输出,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 交易(这是一种特殊的交易),现在我们需要一种通用的普通交易,修改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
{
/**
* 找出地址的所有未花费交易
* @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 协议》,转载必须注明作者和本文链接