Bitcoin P2PKH Transaction Building with Node.js

This post discusses the mechanics of transaction building for Pays to PubKey Hash (P2PKH) transactions.

P2PKH transactions are a standard format to pay to Bitcoin addresses. If you are unfamiliar with this type of transaction, I recommend reading through the transaction section of the Bitcoin Developers Guide.

There are several libraries that can assist with transaction building:

This example is going to do things with raw Node.js JavaScript and the assistance of a few common libraries and assistance from simplified bitcoinjs-lib code.

So let's get the party started.

General Workflow

The workflow for transaction building follows a few steps:

  1. Attach inputs
  2. Attach outputs
  3. Sign required inputs
  4. Obtain the hex-encoded transaction for broadcast to the Bitcoin network!

The libraries mentioned above abstract away many of the complexities associated with Bitcoin transaction building. One of the challenges is managing the state of the inputs as you move from building to signing to output.

Raw Transaction Building

The code listed below takes a functional approach to transaction building to reduce state and make understanding the mechanics easier.

The major caveat is that this code is specifically for transactions with P2PKH inputs and outputs with SIGHASH_ALL signature hash type. Support for other input/output types and signature hash types increases the complexity. We will some examples of that in future articles.

A few libraries are used to assist with the writing of this code:

  • secp256k1: secp256k1 elliptic curve library (native c++ code ported from bitcoin-core)
  • bitcoin-ops: Bitcoin script op codes
  • bip66: strict DER transaction signing utility
  • bs58check: base58 check encoder/decoder
  • varuint-bitcoin: encode/decode varuint values
  • pushdata-bitcoin: library for encoding/decoding push data into bitcoin scripts
  • simple-buffer-cursor: library for reading/writing to buffers

Without further ado:

Step 1: Create a raw transaction object

let tx = { version: 2, locktime: 0, vins: [], vouts: [] };  

This step doesn't have much to it, just an object that returns an object with version 2 transaction, locktime of 0, and two empty arrays for storing the inputs and outputs for the transaction.

Step 2: Add an P2PKH input

The first part has a private key as a 32-byte buffer. We use the secp256k1 library to obtain the public key for this private key. This private key will also be used to sign our transaction once we have built it. A Bitcoin address can be obtained from the public key. We previously sent testnet bitcoins to this address for use in the example!

let privKey = Buffer.from(  
  '60226ca8fb12f6c8096011f36c5028f8b7850b63d495bc45ec3ca478a29b473d',
  'hex'
);
let pubKey = secp256k1.publicKeyCreate(privKey);  

The next step builds the input and attaches it to the transaction.

tx.vins.push({  
  txid: Buffer('cf8597868cec794f9995fad1fb1066f06433332bc56c399c189460e74b7c9dfe', 'hex'),
  vout: 1,
  hash: Buffer('cf8597868cec794f9995fad1fb1066f06433332bc56c399c189460e74b7c9dfe', 'hex').reverse(),
  sequence: 0xffffffff,
  script: p2pkhScript(hash160(pubKey)),
  scriptSig: null,
});

This step references the txid where we want to access the unspent transaction output (UTXO). We also include the vout or the index of the output in the referenced transaction.

We also include a hash property for the transaction. In this example, it is just the reverse of the txid, or more specifically, the txid is the reverse of the hash.

We use the default sequence value of 0xffffffff.

The script is important. This represents the previous output script used in the prior transaction. Because it's P2PKH we know that the script will take the form:

OP_DUP OP_HASH160 [hash160(public_key)] OP_EQUALVERIFY OP_CHECKSIG  

We first call a function to convert the pubkey into a hash160 output. Then we call a function to generate a standard P2PKH script with the hash160 output.

Lastly, we add an empty scriptSig property that will be used for storing the signature value after we have signed the input.

Back to the script section... The hash160 function uses standard Node.js crypto functions and looks like:

function hash160(data) {  
  return ripemd160(sha256(data));
}
function sha256(data) {  
  let hash = crypto.createHash('sha256');
  hash.update(data);
  return hash.digest();
}

function ripemd160(data) {  
  let hash = crypto.createHash('ripemd160');
  hash.update(data);
  return hash.digest();
}

The p2pkhScript leverages the bitcoin-ops library and a compile function based on bitcoinjs-lib to convert the Bitcoin script array into a Buffer:

function p2pkhScript(hash160PubKey) {  
  return compileScript([
    OPS.OP_DUP,
    OPS.OP_HASH160,
    hash160PubKey,
    OPS.OP_EQUALVERIFY,
    OPS.OP_CHECKSIG
  ]);
}

The compileScript function is not that interesting and works by iterating the array, following BIP62.3 to use the minimal op_code, and using pushdata-bitcoin to push the raw buffer data onto the stack.

With this object, we have an input that has a fully build prior output script but it does not have a signature yet. We must wait until we have all the information for the transaction before we can sign the input.

Step 3: Add an output for an address
Outputs are much simpler. We have already seen the mechanics in the input where we build a P2PKH script. We will do the same here, except we will first decode a Bitcoin address to obtain the pubkey hash:

tx.vouts.push({  
  script: p2pkhScript(fromBase58Check('mrz1DDxeqypyabBs8N9y3Hybe2LEz2cYBu').hash),
  value: 900,
});

The fromBase58Check function uses the bs58check library to decode the address. It then splits the address into a version and hash by reading the first byte as the version and the remainder as the hash.

function fromBase58Check(address) {  
  let payload = bs58check.decode(address);
  let version = payload.readUInt8(0);
  let hash = payload.slice(1);
  return { version, hash };
}

The hash is what we pass into the p2pkhScript function that we also used in the input to generate our standard P2PKH script.

Step 4: add a change output
We will also add a change output that will reuse the same pubkey that was inside the input. The bulk of the funds will be sent here back to the originating address.

// 4: add output for change address
tx.vouts.push({  
  script: p2pkhScript(hash160(pubKey)),
  value: 11010000,
});

Step 5: sign the input
Now that the transaction is ready we need to sign the inputs. This is the most complicated step and involves some work to generate the signature.

tx.vins[0].scriptSig = p2pkhScriptSig(signp2pkh(tx, 0, privKey, 0x1), pubKey);  

We have two functions that signp2pkh and p2pkhScriptSig. After these functions are called we assign the result to the scriptSig property of the input. This value stores the completed scriptSig value that is used when the transaction is serialized.

p2pkhScriptSig is relatively simple. It is a function that returns Bitcoin script with the signature and the pubkey for the input. This is a requirement for spending P2PKH inputs. This function looks like:

function p2pkhScriptSig(sig, pubkey) {  
  return compileScript([sig, pubkey]);
}

This function again uses the compileScript function to push the two buffers into standard Bitcoin script format.

More interesting is the signp2pkh function. This example use the SIGHASH_ALL signature hash type. This means that all inputs and outputs are signed. This function is responsible for creating a digital signature that correctly performs that activity. More information about the process can be found in the OP_CHECKSIG documentation.

The code for this was inspired by bitcoinjs-lib. This function operates on a specific input defined by the vindex parameter.

function signp2pkh(tx, vindex, privKey, hashType = 0x01) {  
  let clone = cloneTx(tx);

  // clean up relevant script
  let filteredPrevOutScript = clone.vins[vindex].script.filter(op => op !== OPS.OP_CODESEPARATOR);
  clone.vins[vindex].script = filteredPrevOutScript;

  // zero out scripts of other inputs
  for (let i = 0; i < clone.vins.length; i++) {
    if (i === vindex) continue;
    clone.vins[i].script = Buffer.alloc(0);
  }

  // write to the buffer
  let buffer = txToBuffer(clone);

  // extend and append hash type
  buffer = Buffer.alloc(buffer.length + 4, buffer);

  // append the hash type
  buffer.writeInt32LE(hashType, buffer.length - 4);

  // double-sha256
  let hash = sha256(sha256(buffer));

  // sign input
  let sig = secp256k1.sign(hash, privKey);

  // encode
  return encodeSig(sig.signature, hashType);
}

This method does quite a few things:

  • Clone the tx so we can modify it and serialize the modified transaction
  • Zero out input scripts that are not relevant to the current input
  • Convert the transaction to a buffer and attach signature hash type to the buffer.
  • Perform a double-sha256 hash on the buffer
  • Sign the resulting hash with the private key
  • DER encode the signature r and s values according to BIP66
  • Appending the signature hash type to the DER encoding

As you can see we need to have the input's script available in order to sign the transaction. This is one of the reasons we have to construct a script in step 2 when we build the input.

Step 6: convert to hex
Now that the inputs have been signed, we can convert the transaction to hex.

let result = txToBuffer(tx).toString('hex');  

Astute readers may notice that we use the txToBuffer method during signing. Now that the scriptSig property has been populated on the input it will be used instead of the script property during the serialization process.

function txToBuffer(tx) {  
  let buffer = Buffer.alloc(calcTxBytes(tx.vins, tx.vouts));
  let cursor = new BufferCursor(buffer);

  // version
  cursor.writeInt32LE(tx.version);

  // vin length
  cursor.writeBytes(varuint.encode(tx.vins.length));

  // vin
  for (let vin of tx.vins) {
    cursor.writeBytes(vin.hash);
    cursor.writeUInt32LE(vin.vout);
    if (vin.scriptSig) {
      cursor.writeBytes(varuint.encode(vin.scriptSig.length));
      cursor.writeBytes(vin.scriptSig);
    } else {
      cursor.writeBytes(varuint.encode(vin.script.length));
      cursor.writeBytes(vin.script);
    }
    cursor.writeUInt32LE(vin.sequence);
  }

  // vout length
  cursor.writeBytes(varuint.encode(tx.vouts.length));

  // vouts
  for (let vout of tx.vouts) {
    cursor.writeUInt64LE(vout.value);
    cursor.writeBytes(varuint.encode(vout.script.length));
    cursor.writeBytes(vout.script);
  }

  // locktime
  cursor.writeUInt32LE(tx.locktime);

  return buffer;
}

That's all there is to it. For a complete review of the code, take a look at: https://github.com/bmancini55/crypto-playpen/blob/master/bitcoin2/src/example-p2pkh.js

comments powered by Disqus