赞
踩
源码地址: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('<hr>');
- 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('<br>');
- 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);
- else
- echo "非法";
试想一下,如果你不知道私钥的情况下,你怎么伪造这个公钥的转账?
我这里给你的三个数据,你把其中任何一个数字变动一下都是变成非法。
注意是变动,不是增加,不然会造成字符串格式大小问题。
当然我这只是简单的举个例子,有很多小漏洞。
比如,你可以把这串数据无限复制,那就不停的可以转张三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 < $amount) {
- echo "余额不足";
- exit;
- }
-
- $inputs = [];
- $outputs = [];
-
- /**
- * @var TXOutput $output
- */
- foreach ($validOutputs as $txId => $outsIdx) {
- foreach ($outsIdx as $outIdx) {
- $inputs[] = new TXInput($txId, $outIdx, '', $wallet->publicKey);//根据未花费的output构建对应的input
- }
- }
-
- $outputs[] = TXOutput::NewTxOutput($amount, $to);//构建一笔满足支付的output
- if ($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 ('<hr>');
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('<br>创世块的哈希值是:<br>'.$bc->tips);
- echo('<hr/>所有区块信息:<br>');
-
- foreach ($bc as $block){
- print_r($block);
- echo('<hr>');
- }
-
- }
这会有问题,因为我们调用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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。