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:
- Attach inputs
- Attach outputs
- Sign required inputs
- 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