Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Cryptographic Wallets and Transaction Signing in PHP Blockchain

Tech May 12 3

Environment Setup

The bitwasp/bitcoin v1.0 library is required for elliptic curve cryptography. This version necessitates PHP 7.x and specific extensions such as bcmath and gmp. Install the required extensions and the library via Composer:

sudo apt-get install php7.0-bcmath php7.0-gmp
composer require bitwasp/bitcoin:v1.0 --with-all-dependencies

Cryptography Fundamentals

Public-key cryptography ensures account security. A private key can generate a public key, but the reverse is mathematically infeasible. To prove ownership of a public key (the account), the owner signs transaction data using the private key. Network participants then verify the signature against the public key.

Signing data example:

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

$secretHex = '13a2dd2ddd6d25e6a70daad620ccc0f29d9f2a63fa9f05b30969bde2d6894cd1';
$txPayload = '023570050f103fc492e1cf785272c45ee60f8773d0f48192b8fe50ae1d4eed6d51 transfers 50 coins';

$signatureHex = (new PrivateKeyFactory())
    ->fromHexCompressed($secretHex)
    ->sign(new Buffer($txPayload))
    ->getHex();

Verifying the signature:

use BitWasp\Bitcoin\Signature\SignatureFactory;
use BitWasp\Bitcoin\Key\Factory\PublicKeyFactory;

$sigInstance = SignatureFactory::fromHex($signatureHex);
$pubKeyInstance = (new PublicKeyFactory())->fromHex($pubKeyHex);

$isValid = $pubKeyInstance->verify(new Buffer($txPayload), $sigInstance);

Wallet Architecture

The CryptoWallet class encapsulates key pair generation and address derivation:

class CryptoWallet
{
    public $secretKey;
    public $pubKeyHex;

    public function __construct()
    {
        [$this->secretKey, $this->pubKeyHex] = $this->generateKeyPair();
    }

    public function deriveAddress(): string
    {
        $addrCreator = new AddressCreator();
        $scriptFactory = new P2pkhScriptDataFactory();
        $pubKeyObj = (new PublicKeyFactory())->fromHex($this->pubKeyHex);
        $scriptPubKey = $scriptFactory->convertKey($pubKeyObj)->getScriptPubKey();
        $address = $addrCreator->fromOutputScript($scriptPubKey);
        return $address->getAddress(Bitcoin::getNetwork());
    }

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

    private function generateKeyPair(): array
    {
        $keyFactory = new PrivateKeyFactory();
        $secretObj = $keyFactory->generateCompressed(new Random());
        return [$secretObj->getHex(), $secretObj->getPublicKey()->getHex()];
    }
}

Multiple wallets are managed by WalletManager, which handles persistence:

class WalletManager
{
    public $walletCollection = [];

    public function __construct()
    {
        $this->restoreFromDisk();
    }

    public function addNewWallet(): string
    {
        $wallet = new CryptoWallet();
        $address = $wallet->deriveAddress();
        $this->walletCollection[$address] = $wallet;
        return $address;
    }

    public function persistToDisk()
    {
        $data = serialize($this->walletCollection);
        if (!is_dir(storage_path())) mkdir(storage_path(), 0777, true);
        file_put_contents(storage_path() . '/wallet_data', $data);
    }

    public function restoreFromDisk()
    {
        $file = storage_path() . '/wallet_data';
        $this->walletCollection = file_exists($file) ? unserialize(file_get_contents($file)) : [];
    }

    public function fetchWallet(string $address): CryptoWallet
    {
        if (!isset($this->walletCollection[$address])) exit('Address not found');
        return $this->walletCollection[$address];
    }

    public function listAddresses(): array
    {
        return array_keys($this->walletCollection);
    }
}

Updating Transaction Structures

Transaction inputs must now hold the signature and public key instead of simple script references:

class ChainInput
{
    public $prevTxId;
    public $prevOutIdx;
    public $sig;
    public $pubKey;

    public function __construct(string $txId, int $outIdx, string $sig, string $pubKey)
    {
        $this->prevTxId = $txId;
        $this->prevOutIdx = $outIdx;
        $this->sig = $sig;
        $this->pubKey = $pubKey;
    }

    public function matchesHash(string $pubKeyHash): bool
    {
        $pkObj = (new PublicKeyFactory())->fromHex($this->pubKey);
        return $pkObj->getPubKeyHash()->getHex() === $pubKeyHash;
    }
}

Transaction outputs lock funds to a public key hash:

class ChainOutput
{
    public $amount;
    public $pubKeyHash;

    public function __construct(int $val, string $pubKeyHash)
    {
        $this->amount = $val;
        $this->pubKeyHash = $pubKeyHash;
    }

    public function isLockedBy(string $pubKeyHash): bool
    {
        return $this->pubKeyHash === $pubKeyHash;
    }

    public static function createLocked(int $val, string $address): ChainOutput
    {
        $out = new self($val, '');
        $out->pubKeyHash = $out->lockToAddress($address);
        return $out;
    }

    private function lockToAddress(string $address): string
    {
        $creator = new AddressCreator();
        $inst = $creator->fromString($address);
        $raw = $inst->getScriptPubKey()->getHex(); // Extracts version and checksum
        return substr($raw, 6, strlen($raw) - 10); // Trims to RIPEMD-160 hash
    }
}

Signing and Verifying Transactions

The ChainTransaction class requires methods to sign and verify the payload. A trimmed copy is used for signing to ensure signatures are not part of the signed data:

class ChainTransaction
{
    public $inputs;
    public $outputs;
    public $id;

    public function applySignature(string $secret, array $previousTxs)
    {
        if ($this->isCoinbase()) return;
        $cloneTx = $this->createTrimmedClone();

        foreach ($cloneTx->inputs as $idx => $input) {
            $prevTx = $previousTxs[$input->prevTxId];
            $cloneTx->inputs[$idx]->sig = '';
            $cloneTx->inputs[$idx]->pubKey = $prevTx->outputs[$input->prevOutIdx]->pubKeyHash;
            $cloneTx->computeId();
            $cloneTx->inputs[$idx]->pubKey = '';

            $sigHex = (new PrivateKeyFactory())->fromHexCompressed($secret)->sign(new Buffer($cloneTx->id))->getHex();
            $this->inputs[$idx]->sig = $sigHex;
        }
    }

    public function checkSignature(array $previousTxs): bool
    {
        $cloneTx = $this->createTrimmedClone();

        foreach ($this->inputs as $idx => $input) {
            $prevTx = $previousTxs[$input->prevTxId];
            $cloneTx->inputs[$idx]->sig = '';
            $cloneTx->inputs[$idx]->pubKey = $prevTx->outputs[$input->prevOutIdx]->pubKeyHash;
            $cloneTx->computeId();
            $cloneTx->inputs[$idx]->pubKey = '';

            $sigInst = SignatureFactory::fromHex($input->sig);
            $pubKeyInst = (new PublicKeyFactory())->fromHex($input->pubKey);

            if (!$pubKeyInst->verify(new Buffer($cloneTx->id), $sigInst)) {
                return false;
            }
        }
        return true;
    }

    private function createTrimmedClone(): ChainTransaction
    {
        $ins = [];
        $outs = [];
        foreach ($this->inputs as $in) $ins[] = new ChainInput($in->prevTxId, $in->prevOutIdx, '', '');
        foreach ($this->outputs as $out) $outs[] = new ChainOutput($out->amount, $out->pubKeyHash);
        return new self($ins, $outs);
    }
}

Integrating with the Blockchain

The UTXO transaction builder must retrieve the wallet, find spendable outputs, construct inputs with the public key, and sign the transaction before returning it:

public static function buildTransferTx(string $fromAddr, string $toAddr, int $amount, ChainLedger $ledger): ChainTransaction
{
    $mgr = new WalletManager();
    $wallet = $mgr->fetchWallet($fromAddr);

    [$balance, $validOuts] = $ledger->findSpendableOutputs($wallet->getPubKeyHash(), $amount);
    if ($balance < $amount) exit('Insufficient funds');

    $inputs = [];
    $outputs = [];

    foreach ($validOuts as $txId => $outIdxs) {
        foreach ($outIdxs as $outIdx) {
            $inputs[] = new ChainInput($txId, $outIdx, '', $wallet->pubKeyHex);
        }
    }

    $outputs[] = ChainOutput::createLocked($amount, $toAddr);
    if ($balance > $amount) {
        $outputs[] = ChainOutput::createLocked($balance - $amount, $fromAddr);
    }

    $tx = new ChainTransaction($inputs, $outputs);
    $ledger->signChainTransaction($tx, $wallet->secretKey);
    return $tx;
}

The ledger acts as an intermediary to fetch previous transactions required for signing and verification:

class ChainLedger
{
    public function signChainTransaction(ChainTransaction $tx, string $secret)
    {
        $prevTxs = $this->gatherPreviousTxs($tx);
        $tx->applySignature($secret, $prevTxs);
    }

    public function validateChainTransaction(ChainTransaction $tx): bool
    {
        $prevTxs = $this->gatherPreviousTxs($tx);
        return $tx->checkSignature($prevTxs);
    }

    private function gatherPreviousTxs(ChainTransaction $tx): array
    {
        $prevTxs = [];
        foreach ($tx->inputs as $in) {
            $prevTxs[$in->prevTxId] = $this->locateTransaction($in->prevTxId);
        }
        return $prevTxs;
    }

    public function mineNewBlock(array $transactions): Block
    {
        $lastHash = Cache::get('l');
        if (is_null($lastHash)) exit('Blockchain not initialized');

        foreach ($transactions as $tx) {
            if (!$this->validateChainTransaction($tx)) exit('Invalid transaction detected');
        }

        $block = new Block($transactions, $lastHash);
        $this->tips = $block->hash;
        Cache::forever('l', $block->hash);
        Cache::forever($block->hash, serialize($block));
        return $block;
    }
}

Implementation Validation

To test the wallet and balance functionality, initialize a wallet, clear the old blockchain cache, and create the genesis block. The coinbase transaction must use the updated output structure:

// Creating Coinbase output
$coinbaseOut = ChainOutput::createLocked(self::subsidy, $toAddr);
$coinbaseIn = new ChainInput('', -1, '', $data);

Retrieving the balance requires passing the public key hash rather than the address string:

public static function fetchBalance($address)
{
    $ledger = ChainLedger::getInstance();
    $mgr = new WalletManager();
    $wallet = $mgr->fetchWallet($address);
    $utxos = $ledger->findUTXO($wallet->getPubKeyHash());

    $balance = 0;
    foreach ($utxos as $out) {
        $balance += $out->amount;
    }
    echo $address . ' Balance: ' . $balance;
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.