当前位置:   article > 正文

PHP从零实现区块链(网页版五)地址、密钥和钱包

PHP从零实现区块链(网页版五)地址、密钥和钱包

源码地址: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;

测试代码:

  1. $privateKeyFactory = new PrivateKeyFactory();
  2. $privateKey = $privateKeyFactory->generateCompressed(new Random());
  3. $publicKey = $privateKey->getPublicKey();
  4. echo('<hr>');
  5. 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 下添加如下代码:

  1. $privateKeyFactory = new PrivateKeyFactory();
  2. $privateKey = $privateKeyFactory->generateCompressed(new Random());
  3. $publicKey = $privateKey->getPublicKey();
  4. echo "私钥:".$privateKey->getHex();
  5. echo('<br>');
  6. 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"

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

注意开头一些引用:

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

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

304402200a5c5b671a3601d49955c30c96adf30f99bd1f542f1a04a727800e2adb0cdd2502201b1cecbf6ddc4c3015124d7c38a7928b5e05c3cacb6d06737e19c1dadd81c285

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

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

注意引用:

use BitWasp\Bitcoin\Signature\SignatureFactory;

  1. $signature="304402200a5c5b671a3601d49955c30c96adf30f99bd1f542f1a04a727800e2adb0cdd2502201b1cecbf6ddc4c3015124d7c38a7928b5e05c3cacb6d06737e19c1dadd81c285";
  2. $pubKey="023570050f103fc492e1cf785272c45ee60f8773d0f48192b8fe50ae1d4eed6d51";
  3. $txData="023570050f103fc492e1cf785272c45ee60f8773d0f48192b8fe50ae1d4eed6d51 to zhangsan 50";
  4. $signatureInstance = SignatureFactory::fromHex($signature);
  5. $pubKeyInstance = (new PublicKeyFactory())->fromHex($pubKey);
  6. $bool = $pubKeyInstance->verify(new Buffer($txData), $signatureInstance);
  7. if ($bool==true)
  8. echo ("交易合法,承认".$txData);
  9. else
  10. echo "非法";

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

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

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

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

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

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

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

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

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

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

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

记住公私钥的特性。

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

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

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

  1. class Wallet
  2. {
  3. /**
  4. * @var string $privateKey
  5. */
  6. public $privateKey;
  7. /**
  8. * @var string $publicKey
  9. */
  10. public $publicKey;
  11. /**
  12. * Wallet constructor.
  13. * @throws \BitWasp\Bitcoin\Exceptions\RandomBytesFailure
  14. */
  15. public function __construct()
  16. {
  17. list($privateKey, $publicKey) = $this->newKeyPair();
  18. $this->privateKey = $privateKey;
  19. $this->publicKey = $publicKey;
  20. }
  21. /**
  22. * @return string
  23. * @throws \Exception
  24. */
  25. public function getAddress(): string
  26. {
  27. $addrCreator = new AddressCreator();
  28. $factory = new P2pkhScriptDataFactory();
  29. $scriptPubKey = $factory->convertKey((new PublicKeyFactory())->fromHex($this->publicKey))->getScriptPubKey();
  30. $address = $addrCreator->fromOutputScript($scriptPubKey);
  31. return $address->getAddress(Bitcoin::getNetwork());
  32. }
  33. /**
  34. * @return array
  35. * @throws \BitWasp\Bitcoin\Exceptions\RandomBytesFailure
  36. */
  37. private function newKeyPair(): array
  38. {
  39. $privateKeyFactory = new PrivateKeyFactory();
  40. $privateKey = $privateKeyFactory->generateCompressed(new Random());
  41. $publicKey = $privateKey->getPublicKey();
  42. return [$privateKey->getHex(), $publicKey->getHex()];
  43. }
  44. }

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

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

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

2.Wallets.php

  1. class Wallets
  2. {
  3. /**
  4. * @var Wallet[] $wallets
  5. */
  6. public $wallets;
  7. public function __construct()
  8. {
  9. $this->loadFromFile();
  10. }
  11. public function createWallet(): string
  12. {
  13. $wallet = new Wallet();
  14. $address = $wallet->getAddress();
  15. $this->wallets[$address] = $wallet;
  16. return $address;
  17. }
  18. public function saveToFile()
  19. {
  20. $walletsSer = serialize($this->wallets);
  21. if (!is_dir(storage_path())) {
  22. mkdir(storage_path(), 0777, true);
  23. }
  24. file_put_contents(storage_path() . '/walletFile', $walletsSer);
  25. }
  26. public function loadFromFile()
  27. {
  28. $wallets = [];
  29. if (file_exists(storage_path() . '/walletFile')) {
  30. $contents = file_get_contents(storage_path() . '/walletFile');
  31. if (!empty($contents)) {
  32. $wallets = unserialize($contents);
  33. }
  34. }
  35. $this->wallets = $wallets;
  36. }
  37. public function getWallet(string $from)
  38. {
  39. if (isset($this->wallets[$from])) {
  40. return $this->wallets[$from];
  41. }
  42. echo "钱包不存在该地址";
  43. exit(0);
  44. }
  45. public function getAddresses(): array
  46. {
  47. return array_keys($this->wallets);
  48. }
  49. }

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

createWallet函数,用来创建一个wallet

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

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

getWallet 通过地址查找一个钱包

getAddresses 获得所有钱包地址

3.修改TXInput

  1. class TXInput
  2. {
  3. /**
  4. * @var string $txId
  5. */
  6. public $txId;
  7. /**
  8. * @var int $vOut
  9. */
  10. public $vOut;
  11. /**
  12. * @var string $signature
  13. */
  14. public $signature;
  15. /**
  16. * @var string $pubKey
  17. */
  18. public $pubKey;
  19. public function __construct(string $txId, int $vOut, string $signature, string $pubKey)
  20. {
  21. $this->txId = $txId;
  22. $this->vOut = $vOut;
  23. $this->signature = $signature;
  24. $this->pubKey = $pubKey;
  25. }
  26. /**
  27. * @param string $pubKeyHash
  28. * @return bool
  29. * @throws \Exception
  30. */
  31. public function usesKey(string $pubKeyHash): bool
  32. {
  33. $pubKeyIns = (new PublicKeyFactory())->fromHex($this->pubKey);
  34. return $pubKeyIns->getPubKeyHash()->getHex() == $pubKeyHash;
  35. }
  36. }

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

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

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

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

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

4.修改TXOutput

  1. class TXOutput
  2. {
  3. /**
  4. * @var int $value
  5. */
  6. public $value;
  7. /**
  8. * @var string $pubKeyHash
  9. */
  10. public $pubKeyHash;
  11. public function __construct(int $value, string $pubKeyHash)
  12. {
  13. $this->value = $value;
  14. $this->pubKeyHash = $pubKeyHash;
  15. }
  16. public function isLockedWithKey(string $pubKeyHash): bool
  17. {
  18. return $this->pubKeyHash == $pubKeyHash;
  19. }
  20. public static function NewTxOutput(int $value, string $address)
  21. {
  22. $txOut = new TXOutput($value, '');
  23. $pubKeyHash = $txOut->lock($address);
  24. $txOut->pubKeyHash = $pubKeyHash;
  25. return $txOut;
  26. }
  27. private function lock(string $address): string
  28. {
  29. $addCreator = new AddressCreator();
  30. $addInstance = $addCreator->fromString($address);
  31. $pubKeyHash = $addInstance->getScriptPubKey()->getHex(); // 这是携带版本+后缀校验的值,需要裁剪一下
  32. return $pubKeyHash = substr($pubKeyHash, 6, mb_strlen($pubKeyHash) - 10);
  33. }
  34. }

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

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

isLockedWithKey对比公钥哈希

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

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

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

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

  1. class Transaction {
  2. public function sign(string $privateKey, array $prevTXs)
  3. {
  4. if ($this->isCoinbase()) {
  5. return;
  6. }
  7. $txCopy = $this->trimmedCopy();
  8. foreach ($txCopy->txInputs as $inId => $txInput) {
  9. $prevTx = $prevTXs[$txInput->txId];
  10. $txCopy->txInputs[$inId]->signature = '';
  11. $txCopy->txInputs[$inId]->pubKey = $prevTx->txOutputs[$txInput->vOut]->pubKeyHash;
  12. $txCopy->setId();
  13. $txCopy->txInputs[$inId]->pubKey = '';
  14. $signature = (new PrivateKeyFactory())->fromHexCompressed($privateKey)->sign(new Buffer($txCopy->id))->getHex();
  15. $this->txInputs[$inId]->signature = $signature;
  16. }
  17. }
  18. public function verify(array $prevTXs): bool
  19. {
  20. $txCopy = $this->trimmedCopy();
  21. foreach ($this->txInputs as $inId => $txInput) {
  22. $prevTx = $prevTXs[$txInput->txId];
  23. $txCopy->txInputs[$inId]->signature = '';
  24. $txCopy->txInputs[$inId]->pubKey = $prevTx->txOutputs[$txInput->vOut]->pubKeyHash;
  25. $txCopy->setId();
  26. $txCopy->txInputs[$inId]->pubKey = '';
  27. $signature = $txInput->signature;
  28. $signatureInstance = SignatureFactory::fromHex($signature);
  29. $pubKey = $txInput->pubKey;
  30. $pubKeyInstance = (new PublicKeyFactory())->fromHex($pubKey);
  31. $bool = $pubKeyInstance->verify(new Buffer($txCopy->id), $signatureInstance);
  32. if ($bool == false) {
  33. return false;
  34. }
  35. }
  36. return true;
  37. }
  38. private function trimmedCopy(): Transaction
  39. {
  40. $inputs = [];
  41. $outputs = [];
  42. foreach ($this->txInputs as $txInput) {
  43. $inputs[] = new TXInput($txInput->txId, $txInput->vOut, '', '');
  44. }
  45. foreach ($this->txOutputs as $txOutput) {
  46. $outputs[] = new TXOutput($txOutput->value, $txOutput->pubKeyHash);
  47. }
  48. return new Transaction($inputs, $outputs);
  49. }
  50. }

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

verify(array $prevTXs): bool 函数

trimmedCopy(): Transaction 函数

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

  1. public static function NewUTXOTransaction(string $from, string $to, int $amount, BlockChain $bc): Transaction
  2. {
  3. $wallets = new Wallets();
  4. $wallet = $wallets->getWallet($from);
  5. list($acc, $validOutputs) = $bc->findSpendableOutputs($wallet->getPubKeyHash(), $amount);
  6. if ($acc < $amount) {
  7. echo "余额不足";
  8. exit;
  9. }
  10. $inputs = [];
  11. $outputs = [];
  12. /**
  13. * @var TXOutput $output
  14. */
  15. foreach ($validOutputs as $txId => $outsIdx) {
  16. foreach ($outsIdx as $outIdx) {
  17. $inputs[] = new TXInput($txId, $outIdx, '', $wallet->publicKey);//根据未花费的output构建对应的input
  18. }
  19. }
  20. $outputs[] = TXOutput::NewTxOutput($amount, $to);//构建一笔满足支付的output
  21. if ($acc > $amount) {
  22. $outputs[] = TXOutput::NewTxOutput($acc - $amount, $from); //找零,剩下的output自己
  23. }
  24. $tx = new Transaction($inputs, $outputs);
  25. $bc->signTransaction($tx, $wallet->privateKey);
  26. return $tx;
  27. }

 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

  1. public function mineBlock(array $transactions): Block
  2. {
  3. $lastHash = Cache::get('l');
  4. if (is_null($lastHash)) {
  5. echo "还没有区块链,请先初始化";
  6. exit;
  7. }
  8. foreach ($transactions as $tx) {
  9. if (!$this->verifyTransaction($tx)) {
  10. echo "交易验证失败";
  11. exit(0);
  12. }
  13. }
  14. $block = new Block($transactions, $lastHash);
  15. $this->tips = $block->hash;
  16. Cache::forever('l', $block->hash);
  17. Cache::forever($block->hash, serialize($block));
  18. return $block;
  19. }

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

signTransaction函数在BlockChain里如下:

  1. class BlockChain {
  2. public function signTransaction(Transaction $tx, string $privateKey)
  3. {
  4. $prevTXs = [];
  5. foreach ($tx->txInputs as $txInput) {
  6. $prevTx = $this->findTransaction($txInput->txId);
  7. $prevTXs[$prevTx->id] = $prevTx;
  8. }
  9. $tx->sign($privateKey, $prevTXs);
  10. }
  11. public function verifyTransaction(Transaction $tx): bool
  12. {
  13. $prevTXs = [];
  14. foreach ($tx->txInputs as $txInput) {
  15. $prevTx = $this->findTransaction($txInput->txId);
  16. $prevTXs[$prevTx->id] = $prevTx;
  17. }
  18. return $tx->verify($prevTXs);
  19. }
  20. // 还有些其他方法的修改
  21. }

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

先遍历你的tx每一笔input

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

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

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

  1. public function findTransaction(string $txId): Transaction
  2. {
  3. /**
  4. * @var Block $block
  5. */
  6. foreach ($this as $block) {
  7. foreach ($block->transactions as $tx) {
  8. if ($tx->id == $txId) {
  9. return $tx;
  10. }
  11. }
  12. }
  13. echo "Transaction is not found";
  14. exit(0);
  15. }

然后把找到的这个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 }

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

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

  1. $wallets = new Wallets();
  2. $address = $wallets->createWallet();
  3. $wallets->saveToFile();
  4. echo("Your new address:".$address);
  5. echo ('<hr>');

OK,钱包功能正常:

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

修改get('add')如下代码:

  1. if($request->get('add')!="")
  2. {
  3. //添加区块
  4. $data=$request->get('data');
  5. $time1 = time();
  6. $bc = BlockChain::NewBlockChain($data);
  7. $time2 = time();
  8. $spend = $time2 - $time1;
  9. echo('花费时间(s):'.$spend);
  10. echo('<br>创世块的哈希值是:<br>'.$bc->tips);
  11. echo('<hr/>所有区块信息:<br>');
  12. foreach ($bc as $block){
  13. print_r($block);
  14. echo('<hr>');
  15. }
  16. }

这会有问题,因为我们调用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方法

  1. public function getPubKeyHash(): string
  2. {
  3. $pubKeyIns = (new PublicKeyFactory())->fromHex($this->publicKey);
  4. return $pubKeyIns->getPubKeyHash()->getHex();
  5. }

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

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

  1. public static function getBalance($address)
  2. {
  3. $bc = BlockChain::GetBlockChain();
  4. $wallets = new Wallets();
  5. $wallet = $wallets->getWallet($address);
  6. $UTXOs = $bc->findUTXO($wallet->getPubKeyHash());
  7. $balance = 0;
  8. foreach ($UTXOs as $output) {
  9. $balance += $output->value;
  10. }
  11. echo($address."的余额是:".$balance);
  12. }

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

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

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/288848
推荐阅读
相关标签
  

闽ICP备14008679号