Bitcoin Block Explorer and Wallet

using Node.js

Posted on April 19, 2018 in blockchain, webdev

See it on Github

In order to better comprehend the technical side of the blockchain, I decided to create a simple block explorer using Blockgeeks' courses. I ended up building a wallet too to try and understand all the technical building blocks. (like hashing, public/private keys, locking scripts, utxos, inputs and outputs)

Block Explorer

I used Block Cypher's API to fetch info about the latest blocks. In order to access the api in a clear way, I created a class for it.

class WebAPI {
    constructor(){
        this.api = "https://api.blockcypher.com/v1/btc/";
        this.network = "main";
    }

    changeNetwork(network){
        if(network == "mainnet"){
            this.network = "main";
        }
        else if (network == "testnet"){
            this.network = "test3";
        }
    }

    getLastBlockNumber(){
        var url = this.api + this.network;

        return new Promise((resolve, reject) => {
            request(url, (err, res, body) => {
                if(err) reject(err);
                var result = JSON.parse(body);
                resolve(result.height);
            })
        })
    }

    getBlock(id){
        var url = this.api + this.network + '/blocks/' + id;

        return new Promise((resolve, reject) => {
            request(url, (err, res, body) => {
                if(err) reject(err);
                var result = JSON.parse(body);
                resolve({
                    hash: result.hash,
                    number: result.height,
                    time: result.time
                })
            })
        })
    }
}

They set a low limit of requests to the API, so I just retrieved the latest 5 and displayed them in a table.

var btc = new bitcoin();
btc.getLastBlockNumber().then(function(num){
    for(var i = 0; i < 5; i++){
        btc.getBlock(num-i).then(function(block) {
            printBlocks(block);
        })
    }
})

function printBlocks(block){
    var table = document.getElementById('blocks');
    var row = table.insertRow(-1);
    var cell1 = row.insertCell(0);
    var cell2 = row.insertCell(1);
    var cell3 = row.insertCell(2);
    cell1.innerHTML = block.number;
    cell2.innerHTML = block.hash;
    cell3.innerHTML = block.time;
}

And the end result...

The block height and other metadata can be viewed in the console; I didn't display it on the page.

Bitcoin Wallet

Perhaps the most tedious part of making a wallet is figuring out all the data massaging to correctly format addresses, keys, inputs, and outputs. Some examples...

// keys.js

function createPrivKey() {
    return Buffer.from(getRandomValue(new Uint8Array(32))).toString('hex');
}

function createKeyPair(key = 0) {
    var privateKey = (key === 0)? createPrivKey() : decodePrivKey(key);
    var elliptic = ecurve.getCurveByName('secp256k1');
    var publicKey = elliptic.G.multiply(BigInteger.fromHex(privateKey));
    publicKey = publicKey.getEncoded(true).toString('hex');

    return {private: privateKey, public: publicKey};
}

function generateAddr(publicKey, network="mainnet") {
    var bytes = Buffer.from(publicKey, 'hex');
    var tmp = crypto.createHash('sha256').update(bytes).digest();
    var pubKeyHash = crypto.createHash('rmd160').update(tmp).digest();

    var versionPrefix = (network === "testnet")? "6f" : "00";
    var input = versionPrefix + pubKeyHash.toString('hex');

    bytes = Buffer.from(input, 'hex');
    tmp = crypto.createHash('sha256').update(bytes).digest();
    var checksum = crypto.createHash('sha256').update(tmp).digest();

    // Take first 4 bytes of checksum
    var addr = input + checksum.toString('hex').substr(0,8);

    // Convert to base 58
    bytes = Buffer.from(addr, 'hex');
    addr = base58.encode(bytes);
    return addr;
}

function getKeyHashFromAddr(addr){
    var bytes = base58.decode(addr);
    bytes = bytes.slice(1,21); // remove 1 byte version prefix and 4 byte checksum
    return bytes.toString('hex');
}

// Convert hex key to WIF-compressed key
function encodePrivKey(privateKey, network="mainnet") {
    var prefix = (network === "testnet")? "EF" : "80";
    var newKey = prefix + privateKey + "01";

    // Create checksum
    var bytes = Buffer.from(newKey, 'hex');
    var tmp = crypto.createHash('sha256').update(bytes).digest();
    var checksum = crypto.createHash('sha256').update(tmp).digest();

    // Add first 4 bytes of checksum
    newKey += checksum.toString('hex').substr(0,8);

    // Convert to base58
    bytes = Buffer.from(newKey, 'hex');
    const key = base58.encode(bytes);
    return key;
}

// Convert WIF-compressed key to hex
function decodePrivKey(key) {
    var bytes = base58.decode(key);

    // Remove 1 byte version prefix, 1 byte 01 suffix and 4 byte checksum
    bytes = bytes.slice(1,33);
    return bytes.toString('hex');
}

function getNetworkFromKey(key) {
    var network = "unknown";
    if (key !== 0){
        var first = key.charAt(0);

        if (first === 'K' || first === 'L'){
            network = "mainnet";
        } else if ( first === 'c'){
            network = "testnet";
        }
    }
    return network;
}

function createWallet(network="mainnet", importKey=0) {
    var keys = createKeyPair(importKey);
    var addr = generateAddr(keys.public, network);

    return {
        privateKey: encodePrivKey(keys.private, network),
        publicKey: keys.public,
        address: addr
    }
}
// transactions.js

function ecdsa_sign(tx, priv) {
    // double sha256 has the tx
    var dhash = dsha256(tx);

    // Extract out the 256 bit priv key and convert to bytes
    var key = Buffer.from(keys.decodePrivKey(priv), 'hex');

    // sign the tx with the private key
    return eccrypto.sign(key, dhash);
}

async function signInput(tx, indx, wallet) {
    // Check to make sure publicKeyHash in the locking script matches the hash of the wallet
    var lockingKeyHash = tx.inputs[indx].unlockScript.slice(6,46);
    var pubKeyHash = keys.getKeyHashFromAddr(wallet.address);
    if (lockingKeyHash !== pubKeyHash) {
        throw new Error("Public key didn't match UTXO's locking requirement! Can't spend bitcoin");
    }

    // Make a deep copy of tx obj and clear the script and len field except for the tx currently being signed
    var newtx = JSON.parse(JSON.stringify(tx));
    for (var i = 0; i < newtx.length; i++) {
        if (i != indx) {
            newtx.inputs[i].scriptLength = '00';
            newtx.inputs[i].unlockScript = "";
        }
    }

    // Add temp hashcode to tx and convert it to binary and sign it. Then add hashcode suffic of 01 as well
    newtx.hashcode = "01000000"; // SIGHASH_ALL in little-endian
    var binTx = seralzeObjVal(newtx);
    var signature = await ecdsa_sign(binTx, wallet.privateKey);
    signature = signature.toString('hex') + '01'; // SIGHASH_ALL

    // Create unlock script and insert it to the tx.inputs[indx]
    var sigLenInBytes = toLE(addPadding((signature.length/2).toString(16), 1));
    var pubKeyLenInBytes = toLE(addPadding((wallet.publicKey.length/2).toString(16), 1));
    var unlockScript = sigLenInBytes + signature + pubKeyLenInBytes + wallet.publicKey;
    tx.inputs[indx].unlockScript = unlockScript.toString(16);
    tx.inputs[indx].scriptLength = (unlockScript.length/2).toString(16);
}
//===========================
// Create Transaction
//===========================

function getNewTx(inputs, outputs) {
    return {
        version: "01000000", // 4 byte version in little-endian
        inputcount: toLE(addPadding(inputs.length.toString(16), 1)),
        inputs: inputs,
        outputcount: toLE(addPadding(outputs.length.toString(16), 1)),
        outputs: outputs,
        locktime: "00000000", // 4 byte default to zeroes
    }
}

function createInputs(utxo, amount) {
    var inputs = [];
    var accum = 0;

    // Find a list of inputs that add up to the Amount
    utxo.data.forEach(data => {
        if (accum < amount) {
            accum += data.value;
            inputs.push(data);
        }
    });

    // Create a new array of input data structures
    inputs = inputs.map(tx => {
        var obj = {};
        obj.previousHash = toLE(tx.hash);
        obj.previousIndex = toLE(addPadding(tx.index.toString(16), 4)); // in hex
        obj.scriptLength = (tx.script.length/2).toString(16); // length in bytes
        obj.unlockScript = tx.script; // Set to the locking script for now
        obj.sequencew = 'ffffffff'; // use default value
        return obj;
    })

    inputs.push(accum); // Store value of all inputs as last element
    return inputs;
}

function createSingleOutput(amount, toAddr) {
    // Create locking script
    var pubKeyHash = keys.getKeyHashFromAddr(toAddr);
    var keyHashInBytes = (pubKeyHash.length/2).toString(16);
    var script = opcodes.OP_DUP + opcodes.OP_HASH160 + keyHashInBytes + pubKeyHash + opcodes.OP_EQUALVERIFY + opcodes.OP_CHECKSIG;

    // Create output
    var output = {};
    output.value = toLE(addPadding(amount.toString(16), 8));
    output.length = (script.length/2).toString(16); // length in bytes
    output.script = script;

    return output;
}

function createOutputs(amount, toAddr, inputValue, wallet) {
    var outputs = [];

    // Create normal output
    outputs.push(createSingleOutput(amount, toAddr));

    // Create change output if necessary
    var change = inputValue - amount - getFee();
    if (change > 0) {
        outputs.push(createSingleOutput(change, wallet.address));
    }

    return outputs;
}

async function create(utxo, amount, toAddr, wallet) {
    var inputs = createInputs(utxo, amount);
    var inputValue = inputs.pop();
    var outputs = createOutputs(amount, toAddr, inputValue, wallet);
    var tx = getNewTx(inputs, outputs);
    return tx;

    // Sign all inputs individually
    for (var i = 0; i < inputs.length; i++) {
        await signInput(tx, i, wallet);
    }

    return seralzeObjVal(tx);
}

While I pretty much just followed along with the tutorial, I learned quite a bit about how the pieces all fit together.


comments powered by Disqus