PHP從零實(shí)現(xiàn)區(qū)塊鏈(五)地址、密鑰和錢包

引言

你可能聽說過比特幣是基于密碼學(xué),密碼學(xué)可以用來證明秘密的知識,而不會泄露秘密(數(shù)字簽名),或證明數(shù)據(jù)的真實(shí)性(數(shù)字指紋)。 (也許這就是數(shù)學(xué)的魅力吧)
這些類型的加密證明是比特幣中關(guān)鍵的數(shù)學(xué)工具并在比特幣應(yīng)用程序中被廣泛使用。今天我們來實(shí)現(xiàn)比特幣中用來控制資金的所有權(quán)的密碼學(xué),包括密鑰,地址和錢包。代碼差異較大,具體點(diǎn)擊查看

簡介

比特幣中沒有存儲任何個人帳戶相關(guān)的信息,但是當(dāng)別人發(fā)送一些幣給我時,總要有某種途徑識別出我是交易輸出的所有者(即我擁有在這些輸出上鎖定的幣)。比特幣的所有權(quán)是通過數(shù)字密鑰、比特幣地址和數(shù)字簽名來確定的。

數(shù)字密鑰實(shí)際上并不存儲在網(wǎng)絡(luò)中,而是由用戶生成之后,存儲在一個叫做錢包的文件或簡單的數(shù)據(jù)庫中。存儲在用戶錢包中的數(shù)字密鑰完全獨(dú)立于比特幣協(xié)議,可由用戶的錢包軟件生成并管理,而無需參照區(qū)塊鏈或訪問網(wǎng)絡(luò)。密鑰實(shí)現(xiàn)了比特幣的許多有趣特性,包括去中心化信任和控制、所有權(quán)認(rèn)證和基于密碼學(xué)證明的安全模型。

公鑰加密

公鑰加密(public-key cryptography)算法使用的是成對的密鑰:私鑰+由私鑰衍生出的唯一的公鑰。

私鑰、公鑰、地址
私鑰

私鑰其實(shí)就是一個隨機(jī)選出的數(shù)字而已,私鑰用于生成支付比特幣所必需的簽名以證明對資金的所有權(quán)。所以私鑰一定要保密,不能泄露給第三方。私鑰還必須進(jìn)行備份,以防意外丟失,因?yàn)樗借€一旦丟失就難以復(fù)原,其所保護(hù)的比特幣也將永遠(yuǎn)丟失。

公鑰

公鑰是通過橢圓曲線乘法從私鑰計(jì)算得到的,在數(shù)學(xué)上,這是不可逆轉(zhuǎn)的過程,所以我們無法從公鑰推導(dǎo)出私鑰。具體的數(shù)學(xué)原理就不展開了,有興趣的小伙伴可以去學(xué)習(xí)下。

地址

比特幣地址是一個由數(shù)字和字母組成的字符串,可以與任何想給你比特幣的人分享。地址是由公鑰經(jīng)過哈希計(jì)算得到,我們平時見到的地址是公鑰哈希以后,再加上版本前綴,最后通過 Base58Check 編碼的到的。也就是類似這樣 “1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
”以 1 打頭的地址。

地址生成過程
數(shù)字簽名

在數(shù)學(xué)和密碼學(xué)中,有一個數(shù)字簽名(digital signature)的概念,算法可以保證:

  • 當(dāng)數(shù)據(jù)從發(fā)送方傳送到接收方時,數(shù)據(jù)不會被修改;
  • 數(shù)據(jù)由某一確定的發(fā)送方創(chuàng)建;
  • 發(fā)送方無法否認(rèn)發(fā)送過數(shù)據(jù)這一事實(shí)。

通過在數(shù)據(jù)上應(yīng)用簽名算法(也就是對數(shù)據(jù)進(jìn)行簽名),你就可以得到一個簽名,這個簽名晚些時候會被驗(yàn)證。生成數(shù)字簽名需要一個私鑰,而驗(yàn)證簽名需要一個公鑰。

為了對數(shù)據(jù)進(jìn)行簽名,我們需要下面兩樣?xùn)|西:

  • 要簽名的數(shù)據(jù)
  • 私鑰

應(yīng)用簽名算法可以生成一個簽名,并且這個簽名會被存儲在交易輸入中。為了對一個簽名進(jìn)行驗(yàn)證,我們需要以下三樣?xùn)|西:

  • 被簽名的數(shù)據(jù)
  • 簽名
  • 公鑰

在比特幣中,每一筆交易輸入都會由創(chuàng)建交易的人簽名。在被放入到一個塊之前,必須要對每一筆交易進(jìn)行驗(yàn)證。除了一些其他步驟,驗(yàn)證意味著:

  • 檢查交易輸入有權(quán)使用來自之前交易的輸出
  • 檢查交易簽名是正確的
簽名與驗(yàn)證

現(xiàn)在來回顧一個交易完整的生命周期:

  1. 起初,創(chuàng)世塊里面包含了一個 coinbase 交易。在 coinbase 交易中,沒有輸入,所以也就不需要簽名。coinbase 交易的輸出包含了一個哈希過的公鑰(使用的是 RIPEMD16(SHA256(PubKey)) 算法)
  2. 當(dāng)一個人發(fā)送幣時,就會創(chuàng)建一筆交易。這筆交易的輸入會引用之前交易的輸出。每個輸入會存儲一個公鑰(沒有被哈希)和整個交易的一個簽名。
  3. 比特幣網(wǎng)絡(luò)中接收到交易的其他節(jié)點(diǎn)會對該交易進(jìn)行驗(yàn)證。除了一些其他事情,他們還會檢查:在一個輸入中,公鑰哈希與所引用的輸出哈希相匹配(這保證了發(fā)送方只能花費(fèi)屬于自己的幣);簽名是正確的(這保證了交易是由幣的實(shí)際擁有者所創(chuàng)建)。
  4. 當(dāng)一個礦工準(zhǔn)備挖一個新塊時,他會將交易放到塊中,然后開始挖礦。
  5. 當(dāng)新塊被挖出來以后,網(wǎng)絡(luò)中的所有其他節(jié)點(diǎn)會接收到一條消息,告訴其他人這個塊已經(jīng)被挖出并被加入到區(qū)塊鏈。
  6. 當(dāng)一個塊被加入到區(qū)塊鏈以后,交易就算完成,它的輸出就可以在新的交易中被引用。

地址實(shí)現(xiàn)

我們先從錢包開始,錢包其實(shí)是幫助我們生成并管理密鑰對的地方。
在這之前我們先安裝兩個庫

composer require mdanter/ecc
composer require bitwasp/bitcoin

新建一個 Wallet.phpWallets.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()];
    }
}

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

Wallet 當(dāng)中,我們存儲一對密鑰,newKeyPair() 方法中使用第三方庫(PrivateKeyFactory)創(chuàng)建了私鑰與公鑰,并返回對應(yīng)的十六進(jìn)制字符串。getAddress() 則是從公鑰計(jì)算出地址(解釋下P2pkhScriptDataFactory,P2PKH是比特幣中最常見的交易類型,即支付到一個公鑰哈希,還記得之前說的生成地址時的前綴嗎?類型不同,其實(shí)前綴是不一樣的,這里我們只實(shí)現(xiàn)P2PKH這一種類型的地址就好了)。

Wallets 中,則是創(chuàng)建 Wallet 放入一個map中,并且有一些輔助方法讓我們能持久化錢包數(shù)據(jù)。

下面更新 * TXInput* 與 * TXOutput*

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

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();    // 這是攜帶版本+后綴校驗(yàn)的值,需要裁剪一下
        return $pubKeyHash = substr($pubKeyHash, 6, mb_strlen($pubKeyHash) - 10);
    }
}

注意,現(xiàn)在我們已經(jīng)不再需要 ScriptPubKeyScriptSig 字段,因?yàn)槲覀儾粫?shí)現(xiàn)一個腳本語言。相反,ScriptSig 會被分為 SignaturePubKey 字段,ScriptPubKey 被重命名為 PubKeyHash。我們會實(shí)現(xiàn)跟比特幣里一樣的輸出鎖定/解鎖和輸入簽名邏輯,不同的是我們會通過方法(method)來實(shí)現(xiàn)。

UsesKey 方法檢查輸入使用了指定密鑰來解鎖一個輸出。注意到輸入存儲的是原生的公鑰(也就是沒有被哈希的公鑰),但是這個函數(shù)要求的是哈希后的公鑰。IsLockedWithKey 檢查是否提供的公鑰哈希被用于鎖定輸出。這是一個 UsesKey 的輔助函數(shù),并且它們都被用于 FindUnspentTransactions 來形成交易之間的聯(lián)系。

Lock 只是簡單地鎖定了一個輸出。當(dāng)我們給某個人發(fā)送幣時,我們只知道他的地址,因?yàn)檫@個函數(shù)使用一個地址作為唯一的參數(shù)。然后,地址會被解碼,從中提取出公鑰哈希并保存在 PubKeyHash 字段。

另外為了方便修改,我們創(chuàng)建一個新的 NewTxOutput,外面創(chuàng)建 TXOutput 的地方,都使用該方法代替。

實(shí)現(xiàn)簽名

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

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

        ......

        $tx = new Transaction($inputs, $outputs);
        $bc->signTransaction($tx, $wallet->privateKey);
        return $tx;
    }
}

trimmedCopy 復(fù)制出一個修剪后的交易副本,而不是一個完整交易,然后對交易的每一個輸出構(gòu)建好 pubKey,此時計(jì)算出當(dāng)前交易的 hash 值最為簽名的數(shù)據(jù),在賦值回真實(shí)的交易輸入簽名字段。
$txCopy->txInputs[$inId]->pubKey = ''; 是為了保證每個輸入($txInput)不受上一次迭代的影響。

verify 驗(yàn)證方法當(dāng)然也是一樣,構(gòu)造出簽名數(shù)據(jù),用公鑰驗(yàn)證。只有所有的交易輸入簽名都通過驗(yàn)證時,該方法采訪會 true

修改 NewUTXOTransaction 現(xiàn)在構(gòu)造一筆交易時,需要簽名($bc->signTransaction($tx, $wallet->privateKey);)。

BlockChain

class BlockChain {
    public function mineBlock(array $transactions)
    {
        $lastHash = Cache::get('l');
        if (is_null($lastHash)) {
            echo "還沒有區(qū)塊鏈,請先初始化";
            exit;
        }

        foreach ($transactions as $tx) {
            if (!$this->verifyTransaction($tx)) {
                echo "交易驗(yàn)證失敗";
                exit(0);
            }
        }
    
        ......
    }

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

   // 還有些其他方法的修改
}

現(xiàn)在 mineBlock 時,需要驗(yàn)證交易的每個輸入。還有些其他的修改,比如 findUnspentTransactions findSpentOutputs findSpendableOutputs findUTXO等方法,不再使用地址,而是 pubKeyHash 去尋找未花費(fèi)輸出。

CLI 更新

新建一個 CreateWallet 以及 ListAddresses 命令。

class CreateWallet extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'createwallet';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '創(chuàng)建一個錢包';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $wallets = new Wallets();
        $address = $wallets->createWallet();
        $wallets->saveToFile();
        $this->info("Your new address: {$address}");
    }
}

class ListAddresses extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'listaddresses';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '錢包所有地址';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $wallets = new Wallets();
        $addresses = $wallets->getAddresses();
        foreach ($addresses as $address) {
            $this->info($address);
        }
    }
}

測試

$ php blockchain createwallet
Your new address: 1LRqVSu8Kv9fPgdvXLWP5mTnMxC7TYiYjt

$ php blockchain init-blockchain 1LRqVSu8Kv9fPgdvXLWP5mTnMxC7TYiYjt
init blockchain: ?

$ php blockchain createwallet
Your new address: 1PWiJKQzxdWnePvWjfD3EPnfskAxiGfejX

$ php blockchain send 1LRqVSu8Kv9fPgdvXLWP5mTnMxC7TYiYjt 1PWiJKQzxdWnePvWjfD3EPnfskAxiGfejX 30
send success
0000015150b22a1a85a78f3a408b2caca4a6a7165677654b3f461937eab982eb

$ php blockchain getbalance 1LRqVSu8Kv9fPgdvXLWP5mTnMxC7TYiYjt
balance of address '1LRqVSu8Kv9fPgdvXLWP5mTnMxC7TYiYjt' is: 20

$ php blockchain getbalance 1PWiJKQzxdWnePvWjfD3EPnfskAxiGfejX 
balance of address '1PWiJKQzxdWnePvWjfD3EPnfskAxiGfejX' is: 30

$ php blockchain listaddresses
1LRqVSu8Kv9fPgdvXLWP5mTnMxC7TYiYjt
1PWiJKQzxdWnePvWjfD3EPnfskAxiGfejX

Wow,看起來沒啥問題。

總結(jié)

本次我們實(shí)現(xiàn)了錢包、地址、簽名等功能,再次提醒,代碼變動較大,有啥疑問請點(diǎn)擊這里

在下一節(jié)我們會修改一下交易,讓它看起來更接近真實(shí)的區(qū)塊鏈。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容