源码地址:PHP从零实现区块链(五)地址、密钥和钱包 – 简书

注:本例只是从网页版实现一下原理,源码非本人所写,只是将原帖的源码更改了一下,变成网页版

在开始例子之前,我们需要安装两个库,并了解库中一些函数的用法。

我们先进入mylaravel6 目录,然后输入:

composer require bitwasp/bitcoin

安装 bitwasp/bitcoin库。

但是报一堆错,最下面有这两句:

Alternatively, you can run Composer with `–ignore-platform-req=ext-mcrypt –ignore-platform-req=ext-bcmath` to temporarily ignore these required extensions.
可以看出,是缺少php扩展库,mcrypt和bcmath。

我们按照以前安装gmp库的方法,安装这两个库。

依次输入如下两个命令:

sudo apt-get install php8.1-bcmath

sudo apt-get install php8.1-mcrypt

然后再composer require bitwasp/bitcoin

这次OK,不报错,如下:

我们再看看,vendor目录下已经有了bitwasp库。

关于库的详细说明,我们可以去bitwasp/bitcoin – Packagist这个网站查找,输入库名即可,然后在右边有github相关的地址,可以去看那里看库的源码。

比如下面用到的PrivateKeyFactory类,你可以去BitWasp\Bitcoin\Key\Factory这个目录下查看。

或者vendor对应的目录下也可查看。

接着我们来测试使用一下:

先声明引用:

use BitWasp\Bitcoin\Key\PrivateKeyFactory;

测试代码:

 $privateKeyFactory = new PrivateKeyFactory();$privateKey = $privateKeyFactory->generateCompressed(new Random());$publicKey = $privateKey->getPublicKey();echo('
');echo $privateKey->getHex();

但是报错:

这是由于版本不对,我们之前安装的是0.35版本,privateKeyFactory类下没有generateCompressed这个函数。

这个在1.0版本才有。

所以我们得重新安装bitwasp,并指定版本,如下命令:

composer require bitwasp/bitcoin:”v1.0″

但是报错:found lastguest/murmurhash[2.0.0] but the package is fixed to 1.3.0

需要依赖库为2.0

我们加上命令更新依赖库:

composer require bitwasp/bitcoin:”v1.0″ –with-all-dependencies
接着报错:

lastguest/murmurhash 2.0.0 requires php ^7 -> your php version (8.1.27) does not satisfy that requirement.
说我的PHP版本不对。

后面简单的尝试了下其他安装方法,没能解决个问题,那只能安装一个php7.0版本了。

关于怎么安装,可以参考我这篇教程:ubuntu下安装两种版本laravel框架和php-CSDN博客

安装好后,我们进入php 7.0版本的laravel框架,并将我们的2.2 composer.phar放到这个laravelphp7下,输入命令:

/usr/bin/php7.0 composer.phar require bitwasp/bitcoin:”v1.0″ –with-all-dependencies
再次安装bitwasp/bitcoin

报错:

需要我们先安装gmp扩展和mdanter/ecc 0.5版本。

安装gmp

sudo apt-get install php7.0-gmp

安装mdanter/ecc 0.5

/usr/bin/php7.0 composer.phar require mdanter/ecc:”v0.5″
接着:

/usr/bin/php7.0 composer.phar require bitwasp/bitcoin:”v1.0″ –with-all-dependencies

这次OK。

我们来调用测试一下,先把我们的AppController.php文件搬过去。

然后web.php下添加路由

Route::get(‘/app’, ‘AppController@app’);

(注意添加的代码跟之前的不一样,是因为版本问题,具体可以看我之前的文章)

开头引用:

use BitWasp\Bitcoin\Key\Factory\PrivateKeyFactory;
use BitWasp\Bitcoin\Crypto\Random\Random;

app 下添加如下代码:

$privateKeyFactory = new PrivateKeyFactory();$privateKey = $privateKeyFactory->generateCompressed(new Random());$publicKey = $privateKey->getPublicKey();echo "私钥:".$privateKey->getHex();echo('
');echo "公钥:".$publicKey->getHex();

OK,测试正常,生成了16进制格式的64个字符的私钥,和66个字符的公钥。

当然还有另外几种格式,这里就不介绍了。我们只用一种就可以了。

另外公钥是从私钥计算出来的,但是无法从公钥反推出私钥,记住这个特性,很重要。

后面代码搬迁的详细过程就不写了,基本是把我前面写的文章流程重走一遍,有问题可以去看我之前写的。

另:迁移过程碰到一个问题,报没获得块对象类型不正确。

经查是在这个版本cache::put不设置过期时间,就不起作用了。

我们可以将所有的cache::put用cache::forever代替解决该问题。

然后测试运行正常,迁移成功:

OK,总算搞定了。

接下来说一下,私钥和公钥是怎么实现账户功能的。

它的原理是什么,因为私钥和公钥的一个特性。

即:用私钥加密后的数据,只能用它的公钥来解密。

而公钥是用私钥生成的,但无法用公钥推算出私钥,从数学角度上,保证了安全性,只能穷举破解,但需要的算力是庞大的,等于不可能。

好,设想让你来设计,你怎么利用这个特性实现账户功能。

用公钥作帐户,这个没问题。

然后再设计,证明我是这个账户的所有人。

通过用私钥加密一段数据发送给你,我提前告诉你解密后的数据,然后你用公钥解密这个数据,如果和我发给你的解密数据对应上了。你就是这个账户的所有人?

漏洞太多,首先,第三方知道你的公钥,先随便弄一段数据A,用公钥解密,生成数据B。

然后把A,和B告诉你。你用公钥进行验证,确实是对的。

这个显然是不可行的。

所以正常设计是,用加密交易数据来证明。

我们来生成一对公私钥实验一下,生成方式参考前面的代码,假设生成这样一对:

私钥:13a2dd2ddd6d25e6a70daad620ccc0f29d9f2a63fa9f05b30969bde2d6894cd1
公钥:023570050f103fc492e1cf785272c45ee60f8773d0f48192b8fe50ae1d4eed6d51

我们直接用公钥做账号,要转给zhangsan 50个币。(这里的zhangsan也要用公钥,为了简便这样写了)

数据就是这样:

“023570050f103fc492e1cf785272c45ee60f8773d0f48192b8fe50ae1d4eed6d51 to zhangsan 50”

接着我用私钥签名(直接采用库中函数,已经做好签名功能了)

注意开头一些引用:

$privateKeyhex="13a2dd2ddd6d25e6a70daad620ccc0f29d9f2a63fa9f05b30969bde2d6894cd1"; //私钥$txData="023570050f103fc492e1cf785272c45ee60f8773d0f48192b8fe50ae1d4eed6d51 to zhangsan 50";//数据$signature = (new PrivateKeyFactory())->fromHexCompressed($privateKeyhex)->sign(new Buffer($txData))->getHex(); //获得签名echo $signature;

得到这样一个签名(加密数据)

304402200a5c5b671a3601d49955c30c96adf30f99bd1f542f1a04a727800e2adb0cdd2502201b1cecbf6ddc4c3015124d7c38a7928b5e05c3cacb6d06737e19c1dadd81c285

接着我把交易数据给你,公钥给你,还有签名给你,你这样进行验证:

用公钥解密签名,得到数据对比交易数据是否一致。

注意引用:

use BitWasp\Bitcoin\Signature\SignatureFactory;

$signature="304402200a5c5b671a3601d49955c30c96adf30f99bd1f542f1a04a727800e2adb0cdd2502201b1cecbf6ddc4c3015124d7c38a7928b5e05c3cacb6d06737e19c1dadd81c285";$pubKey="023570050f103fc492e1cf785272c45ee60f8773d0f48192b8fe50ae1d4eed6d51";$txData="023570050f103fc492e1cf785272c45ee60f8773d0f48192b8fe50ae1d4eed6d51 to zhangsan 50";$signatureInstance = SignatureFactory::fromHex($signature);$pubKeyInstance = (new PublicKeyFactory())->fromHex($pubKey);$bool = $pubKeyInstance->verify(new Buffer($txData), $signatureInstance);if ($bool==true) echo ("交易合法,承认".$txData);elseecho "非法";

试想一下,如果你不知道私钥的情况下,你怎么伪造这个公钥的转账?

我这里给你的三个数据,你把其中任何一个数字变动一下都是变成非法。

注意是变动,不是增加,不然会造成字符串格式大小问题。

当然我这只是简单的举个例子,有很多小漏洞。

比如,你可以把这串数据无限复制,那就不停的可以转张三50个币。但别忘了。

在真实的区块中,首先你的输入引用的是之前的output,你只要第一次output已经被花费过了。

后面你再怎么复制同样的交易,都是不被承认的。

第二:你可以用随便生成一串数据充当signature,然后用公钥解密,充当txData。

然后你也得到了我这里代码的合法承认,但是你的这串txData是乱码,不是正常的交易,也不被承认。

第三:好那我制造一个正常的txData,但是你的签名怎么获得呢?用公钥加解密都不行。

因为这个数据再用公钥解密都不是原来的数据,从而不被承认。只能用私钥。

记住公私钥的特性。

好,了解了上面这些东西,我们来正式研究代码吧,因为你已经有了前面几章的基础,除了重点部分,其它我都只大概说一下。

ps:如果提示你缺少某些类的话,可以去原文章的github查看引用了哪些。

1.创建一个Wallet类,Wallet.php

class Wallet{/** * @var string $privateKey */public $privateKey;/** * @var string $publicKey */public $publicKey;/** * Wallet constructor. * @throws \BitWasp\Bitcoin\Exceptions\RandomBytesFailure */public function __construct(){list($privateKey, $publicKey) = $this->newKeyPair();$this->privateKey = $privateKey;$this->publicKey = $publicKey;}/** * @return string * @throws \Exception */public function getAddress(): string{$addrCreator = new AddressCreator();$factory = new P2pkhScriptDataFactory();$scriptPubKey = $factory->convertKey((new PublicKeyFactory())->fromHex($this->publicKey))->getScriptPubKey();$address = $addrCreator->fromOutputScript($scriptPubKey);return $address->getAddress(Bitcoin::getNetwork());}/** * @return array * @throws \BitWasp\Bitcoin\Exceptions\RandomBytesFailure */private function newKeyPair(): array{$privateKeyFactory = new PrivateKeyFactory();$privateKey = $privateKeyFactory->generateCompressed(new Random());$publicKey = $privateKey->getPublicKey();return [$privateKey->getHex(), $publicKey->getHex()];}}

这个Wallet很简单,作用就是通过构造函数调用我们扩展包的方法创建一对公私钥,然后把它们存在$privateKey和$publicKey里。

最后还有个函数,getAddress通过其公钥生成一个标准的比特币地址。

就这三个数据,公私钥和地址。

2.Wallets.php

class Wallets{/** * @var Wallet[] $wallets */public $wallets;public function __construct(){$this->loadFromFile();}public function createWallet(): string{$wallet = new Wallet();$address = $wallet->getAddress();$this->wallets[$address] = $wallet;return $address;}public function saveToFile(){$walletsSer = serialize($this->wallets);if (!is_dir(storage_path())) {mkdir(storage_path(), 0777, true);}file_put_contents(storage_path() . '/walletFile', $walletsSer);}public function loadFromFile(){$wallets = [];if (file_exists(storage_path() . '/walletFile')) {$contents = file_get_contents(storage_path() . '/walletFile');if (!empty($contents)) {$wallets = unserialize($contents);}}$this->wallets = $wallets;}public function getWallet(string $from){if (isset($this->wallets[$from])) {return $this->wallets[$from];}echo "钱包不存在该地址";exit(0);}public function getAddresses(): array{return array_keys($this->wallets);}}

这个wallets类,顾名思义,就是用来管理wallet类的,主要的功能就是把所有的wallet数据存在文件里,然后可以从磁盘读取。然后可以进行创建wallet和查找wallet。

createWallet函数,用来创建一个wallet

saveToFile将所有钱包序列化后存储到文件里。

loadFromFile 从文件中读取所有钱包(构造函数自动调用)

getWallet 通过地址查找一个钱包

getAddresses 获得所有钱包地址

3.修改TXInput

class TXInput{/** * @var string $txId */public $txId;/** * @var int $vOut */public $vOut;/** * @var string $signature */public $signature;/** * @var string $pubKey */public $pubKey;public function __construct(string $txId, int $vOut, string $signature, string $pubKey){$this->txId = $txId;$this->vOut = $vOut;$this->signature = $signature;$this->pubKey = $pubKey;}/** * @param string $pubKeyHash * @return bool * @throws \Exception */public function usesKey(string $pubKeyHash): bool{$pubKeyIns = (new PublicKeyFactory())->fromHex($this->pubKey);return $pubKeyIns->getPubKeyHash()->getHex() == $pubKeyHash;}}

一笔txinput现在变成这样,四个变量,txId和vout不变,交易区块id和vout索引,定位到一笔output。而pubkey是这笔input对应的公钥,用这个做账户地址标识。signature就是这笔交易的签名。保证这笔交易合法和确定是pubkey所有人创建的。

构造函数,传这四个变量的参数。

usesKey函数跟之前的anUnlockOutputWit函数差不多,只不过现在是通过对比公钥哈希来实现的。

注意这个哈希不是256哈希,而是最终公钥RIPEMD-160哈希 20个字节,产生40个16进制的字符。

你们可以调用$pubKeyIns->getPubKeyHash()->getHex()看产生的是什么。

4.修改TXOutput

class TXOutput{/** * @var int $value */public $value;/** * @var string $pubKeyHash */public $pubKeyHash;public function __construct(int $value, string $pubKeyHash){$this->value = $value;$this->pubKeyHash = $pubKeyHash;}public function isLockedWithKey(string $pubKeyHash): bool{return $this->pubKeyHash == $pubKeyHash;}public static function NewTxOutput(int $value, string $address){$txOut = new TXOutput($value, '');$pubKeyHash = $txOut->lock($address);$txOut->pubKeyHash = $pubKeyHash;return $txOut;}private function lock(string $address): string{$addCreator = new AddressCreator();$addInstance = $addCreator->fromString($address);$pubKeyHash = $addInstance->getScriptPubKey()->getHex();// 这是携带版本+后缀校验的值,需要裁剪一下return $pubKeyHash = substr($pubKeyHash, 6, mb_strlen($pubKeyHash) - 10);}}

txoutput还是两个变量,但是地址变成了$pubKeyHash,公钥哈希,注意了这里跟txinput不一样,txinput存的是公钥,因为是好解密,毕竟是用公钥来解密,他的哈希可没这个功能。

它的构造函数传这两个变量。

isLockedWithKey对比公钥哈希

NewTxOutput创建一笔output,但是参数传的是地址,我们需要的是公钥哈希。

所以在lock函数,是通过地址产生公钥哈希 是RIPEMD-160的哈希,跟txinput一样。

接下来是关键部分,签名和验证

首先我们在Transaction类添加如下几个函数:

class Transaction {public function sign(string $privateKey, array $prevTXs){if ($this->isCoinbase()) {return;}$txCopy = $this->trimmedCopy();foreach ($txCopy->txInputs as $inId => $txInput) {$prevTx = $prevTXs[$txInput->txId];$txCopy->txInputs[$inId]->signature = '';$txCopy->txInputs[$inId]->pubKey = $prevTx->txOutputs[$txInput->vOut]->pubKeyHash;$txCopy->setId();$txCopy->txInputs[$inId]->pubKey = '';$signature = (new PrivateKeyFactory())->fromHexCompressed($privateKey)->sign(new Buffer($txCopy->id))->getHex();$this->txInputs[$inId]->signature = $signature;}}public function verify(array $prevTXs): bool{$txCopy = $this->trimmedCopy();foreach ($this->txInputs as $inId => $txInput) {$prevTx = $prevTXs[$txInput->txId];$txCopy->txInputs[$inId]->signature = '';$txCopy->txInputs[$inId]->pubKey = $prevTx->txOutputs[$txInput->vOut]->pubKeyHash;$txCopy->setId();$txCopy->txInputs[$inId]->pubKey = '';$signature = $txInput->signature;$signatureInstance = SignatureFactory::fromHex($signature);$pubKey = $txInput->pubKey;$pubKeyInstance = (new PublicKeyFactory())->fromHex($pubKey);$bool = $pubKeyInstance->verify(new Buffer($txCopy->id), $signatureInstance);if ($bool == false) {return false;}}return true;}private function trimmedCopy(): Transaction{$inputs = [];$outputs = [];foreach ($this->txInputs as $txInput) {$inputs[] = new TXInput($txInput->txId, $txInput->vOut, '', '');}foreach ($this->txOutputs as $txOutput) {$outputs[] = new TXOutput($txOutput->value, $txOutput->pubKeyHash);}return new Transaction($inputs, $outputs);}}

sign(string $privateKey, array $prevTXs)函数

verify(array $prevTXs): bool 函数

trimmedCopy(): Transaction 函数

这些函数将在后面解释,我们还需要Transaction里修改NewUTXOTransaction函数:

public static function NewUTXOTransaction(string $from, string $to, int $amount, BlockChain $bc): Transaction{$wallets = new Wallets();$wallet = $wallets->getWallet($from);list($acc, $validOutputs) = $bc->findSpendableOutputs($wallet->getPubKeyHash(), $amount);if ($acc  $outsIdx) {foreach ($outsIdx as $outIdx) {$inputs[] = new TXInput($txId, $outIdx, '', $wallet->publicKey);//根据未花费的output构建对应的input}}$outputs[] = TXOutput::NewTxOutput($amount, $to);//构建一笔满足支付的outputif ($acc > $amount) {$outputs[] = TXOutput::NewTxOutput($acc - $amount, $from); //找零,剩下的output自己}$tx = new Transaction($inputs, $outputs);$bc->signTransaction($tx, $wallet->privateKey);return $tx;}

NewUTXOTransaction(string $from, string $to, int $amount, BlockChain $bc): Transaction

这个函数在前面说过,是用来创建一笔正常某人给某人的转账交易,建立这样一个tx交易区块。

这里的from和to都是地址参数。

所以开头from就是

$wallets = new Wallets();//加载钱包

$wallet = $wallets->getWallet($from);//得到对应form地址的wallet

然后$bc->findSpendableOutputs($wallet->getPubKeyHash(), $amount)

这里的$wallet->getPubKeyHash()获得公钥哈希作参数。

然后:

$inputs[] = new TXInput($txId, $outIdx, ”, $wallet->publicKey);//根据未花费的output构建对应的input

建立的这笔input有三个参数了,定位到一笔output有了,这个地址的公钥也有了。但还差个签名。

而to就是:

$outputs[] = TXOutput::NewTxOutput($amount, $to);//构建一笔满足支付的output
if ($acc > $amount) {
$outputs[] = TXOutput::NewTxOutput($acc – $amount, $from); //找零,剩下的output自己
}

调用 NewTxOutput方法创建output,前面说过,NewTxOutput方法,会将地址转换成公钥哈希。

所以此时获得的outputs[]就是完全体了。后面不需要再更改。

好,构建好后,我们说过input还缺少签名,并不能提交到链上去,所以下面

$tx = new Transaction($inputs, $outputs); //根据创建好的inputs和outputs构建一个交易区块
$bc->signTransaction($tx, $wallet->privateKey); //根据私钥给这笔tx签名
return $tx;

这样的$tx就有了签名,然后你才可以调用:

$bc->mineBlock([$tx]); 这个将这个交易添加到链上区块里去。(在mineBlock里也会验证签名)

BlockChain.php

 public function mineBlock(array $transactions): Block{$lastHash = Cache::get('l');if (is_null($lastHash)) {echo "还没有区块链,请先初始化";exit;}foreach ($transactions as $tx) {if (!$this->verifyTransaction($tx)) {echo "交易验证失败";exit(0);}}$block = new Block($transactions, $lastHash);$this->tips = $block->hash;Cache::forever('l', $block->hash);Cache::forever($block->hash, serialize($block));return $block;}

好,我们先来单独看一下signTransaction是怎么给$tx签名的:

signTransaction函数在BlockChain里如下:

class BlockChain {public function signTransaction(Transaction $tx, string $privateKey){$prevTXs = [];foreach ($tx->txInputs as $txInput) {$prevTx = $this->findTransaction($txInput->txId);$prevTXs[$prevTx->id] = $prevTx;}$tx->sign($privateKey, $prevTXs);}public function verifyTransaction(Transaction $tx): bool{$prevTXs = [];foreach ($tx->txInputs as $txInput) {$prevTx = $this->findTransaction($txInput->txId);$prevTXs[$prevTx->id] = $prevTx;}return $tx->verify($prevTXs);} // 还有些其他方法的修改}

你把我创建好的tx和privateKey,传进去后,它是这样拆分签名的:

先遍历你的tx每一笔input

然后通过这笔input的指向,找到对应的output,并从区块中加载这笔output所在的交易区块,给prevTx(Transaction类型)。(注意这个prevTx,只是含有这笔output不是独有,也可能还有其它的output,只是定义到这一个交易区块范围)

实现语句 $prevTx = $this->findTransaction($txInput->txId);

其中findTransaction也是在BlockChina里,代码如下:

public function findTransaction(string $txId): Transaction{/** * @var Block $block */foreach ($this as $block) {foreach ($block->transactions as $tx) {if ($tx->id == $txId) {return $tx;}}}echo "Transaction is not found";exit(0);}

然后把找到的这个prevTx放到prevTxs数组里, 并且以它的ID作索引。

$prevTXs[$prevTx->id] = $prevTx;

好,到了这里你应该明白prevTXs是什么,就是找到了你这笔tx下的所有input指向的交易区块。

然后调用:

$tx->sign($privateKey, $prevTXs);

进行具体签名。

而sign函数,在前面代码已经给出了,它是这样的。

在里面先将你的tx用trimmedCopy复制一份,因为你们是tx->sign调用的,所以里面的this就是你的原本那份tx。

关于trimmedCopy函数,前面也已经给出了,具体看代码。

它也不是完整的复制,input只复制了id和vout,剩下两个变量都为空。

在sign里面它,是这样的,input有了三个变量,就setid一次,然后生成一个签名,注意此setid并不是最终交易区块的id,这个是为保证每笔input都有一个独一无二的签名。然后继续下一个setid

然后pubkey变量要注意的是用作签名的是公钥哈希,这个不重要,重要的是你解密时对应起来就可以了。

而这个也是副本,也不最终影响存储时的公钥。

具体看代码:

$txCopy->txInputs[$inId]->pubKey = $prevTx->txOutputs[$txInput->vOut]->pubKeyHash;

$txCopy->setId();

txCopy下三个参数都有值后,就setid,然后把得到这个id做要签名的数据。

setid是哈希交易数据得来的哈希值,sign id就相当于是把交易数据当加密后的签名。

$signature = (new PrivateKeyFactory())->fromHexCompressed($privateKey)->sign(new Buffer($txCopy->id))->getHex();

然后你就得到$signature赋给本体this

$this->txInputs[$inId]->signature = $signature;

这样一翻操作后,你的交易有了签名就完整了,可以添加到区块上去了。

验证方面:

前面说过,接着就会调用mineBlock,这个函数的代码在前面也给出,我们来看一下:

重点是这句:

foreach ($transactions as $tx) {
if (!$this->verifyTransaction($tx)) {
echo “交易验证失败”;
exit(0);
}

调用verifyTransaction函数来验证,这个函数原理跟signTransaction差不多,得到你的inputs,然后生成prveTXS,里面这一句不同:

return $tx->verify($prevTXs);

我们来看看verify函数,跟sign也差不多逻辑,同样的操作,先是生成id,区别是,它用公钥解密你的签名,然后对比跟生成的id是否匹配,匹配,则所有的都是对的,交易合法。

$bool = $pubKeyInstance->verify(new Buffer($txCopy->id), $signatureInstance);

if ($bool == false) return false}

好,所有的东西,都准备的差不多后,我们来测试一下,注意测试前将原来的区块缓存都删掉,因为代码变动,区块结构不一样了,不匹配原来的。

然后我们来重新生成 ,先创建一个地址,测试代码:

 $wallets = new Wallets();$address = $wallets->createWallet();$wallets->saveToFile();echo("Your new address:".$address);echo ('
');

OK,钱包功能正常:

接下来我们创建创世块,给我们的这个地址50个币。

修改get(‘add’)如下代码:

if($request->get('add')!=""){//添加区块$data=$request->get('data');$time1 = time();$bc = BlockChain::NewBlockChain($data);$time2 = time();$spend = $time2 - $time1;echo('花费时间(s):'.$spend);echo('
创世块的哈希值是:
'.$bc->tips);echo('
所有区块信息:
'); foreach ($bc as $block){print_r($block);echo('
'); } }

这会有问题,因为我们调用NewCoinbaseTX创建coinbase交易时,

$txOut = new TXOutput(self::subsidy, $to);

还是用的构造函数创建的output,现在应改为:

$txOut = TXOutput::NewTxOutput(self::subsidy, $to);

而input现在有四个变量

$txIn = new TXInput(”, -1, $data);

改为:

$txIn = new TXInput(”, -1, ”, $data);

接着原来页面调用:

OK,成功。

接下来我们要测试获取余额功能,因为余额功能用的是这个:

$UTXOs = $bc->findUTXO($address);

而findUTXO是直接根据地址寻找,现在我们需要根据公钥哈希来寻找,所以wallet里新增getPubKeyHash方法

 public function getPubKeyHash(): string{$pubKeyIns = (new PublicKeyFactory())->fromHex($this->publicKey);return $pubKeyIns->getPubKeyHash()->getHex();}

修改findUTXO方法,和BlockChain里所有txinput和txoutput调用canUnlockOutputWith和canBeUnlockedWith改为usesKey和isLockedWithKey,因为我们之前已经把它们变了个名字了。

改完后,更改getBalance如下代码:

 public static function getBalance($address) {$bc = BlockChain::GetBlockChain();$wallets = new Wallets();$wallet = $wallets->getWallet($address);$UTXOs = $bc->findUTXO($wallet->getPubKeyHash());$balance = 0;foreach ($UTXOs as $output) { $balance += $output->value; }echo($address."的余额是:".$balance); }

然后用原来的页面测试即可

OK:

接着是转账,我们用之前的代码再创建一个地址后,直接用原来的页面测试即可,因为交易部分代码我们提前改过了。

测试也OK:

好了原理就是这样,因为我们创建的钱包在本地,是直接调用wallet加载的私钥,所以这里还是可以用地址直接转账。

你们可以自行修改这样,把这个页面设权限。只有自己检测时能用。

然后再创建另一个command.php页面,需要私钥才能转账,通过从post获取私钥的方式。

当然我只是提个大概的思路,还是不安全的,最好是本地生成签名,然后post过去验证。

你们可以去看原帖的github源码,有没这方面相关的完善。

或者看go语言那版本,因为还有两部分没写,完善交易默克尔树,挖矿,还有p2p网络这些的。

本系列教程就暂时到此为止了,因为原文章只有五章,以后看情况也许会来完善。

谢谢。

后附源码下载:

csdn:https://download.csdn.net/download/d3582077/88795239

github:https://github.com/Bczheng1/php-chainblock-web