Transaction Construction
Building Bitcoin transactions from scratch requires understanding inputs, outputs, fees, and signing. This guide covers the complete process from UTXO selection to broadcasting.
Transaction Lifecycle
Understanding the complete lifecycle of a transaction helps contextualize the construction process:
- Creation: User constructs a transaction specifying inputs (UTXOs to spend) and outputs (recipient addresses and amounts)
- Signing: User signs the transaction with their private key, proving ownership of the inputs
- Broadcasting: Signed transaction is sent to the network
- Mempool: Transaction waits in the mempool (memory pool) of unconfirmed transactions
- Selection: A miner selects the transaction (typically prioritizing higher fees)
- Inclusion: Transaction is included in a candidate block
- Mining: Miner finds valid proof-of-work for the block
- Propagation: New block spreads across the network
- Confirmation: Each subsequent block adds another confirmation, increasing security
A transaction with 6 confirmations is generally considered irreversible.
Transaction Structure
Components
Transaction
├── Version (4 bytes)
├── Marker & Flag (SegWit only, 2 bytes)
├── Input Count (varint)
├── Inputs
│ ├── Previous TXID (32 bytes)
│ ├── Output Index (4 bytes)
│ ├── Script Length (varint)
│ ├── ScriptSig (variable)
│ └── Sequence (4 bytes)
├── Output Count (varint)
├── Outputs
│ ├── Value (8 bytes)
│ ├── Script Length (varint)
│ └── ScriptPubKey (variable)
├── Witness (SegWit only)
│ └── Witness data per input
└── Locktime (4 bytes)
Byte Order: Most numeric fields (version, value, locktime, sequence, output index) are encoded in little endian. However, transaction IDs (TXIDs) and block hashes are typically displayed in big endian (reversed) for readability, even though they're stored internally in little endian. When working with raw transaction data, the [::-1] reversal in Python (or equivalent) converts between these formats.
Size Calculations
Virtual size (vbytes) = (base_size × 3 + total_size) / 4
Building Transactions
Input Selection
Coin Selection Algorithms
Largest First
function largestFirst(utxos: UTXO[], target: number): UTXO[] {
// Sort by value descending
const sorted = [...utxos].sort((a, b) => b.value - a.value);
const selected: UTXO[] = [];
let total = 0;
for (const utxo of sorted) {
selected.push(utxo);
total += utxo.value;
if (total >= target) break;
}
return total >= target ? selected : [];
}
Branch and Bound (Exact Match)
function branchAndBound(utxos: UTXO[], target: number, maxTries = 100000): UTXO[] | null {
let tries = 0;
let bestSelection: UTXO[] | null = null;
let bestWaste = Infinity;
function search(index: number, selected: UTXO[], total: number): void {
if (tries++ > maxTries) return;
// Found exact match
if (total === target) {
bestSelection = [...selected];
bestWaste = 0;
return;
}
// Over target, calculate waste
if (total > target) {
const waste = total - target;
if (waste < bestWaste) {
bestSelection = [...selected];
bestWaste = waste;
}
return;
}
// Try including next UTXO
if (index < utxos.length) {
// Include
selected.push(utxos[index]);
search(index + 1, selected, total + utxos[index].value);
selected.pop();
// Exclude
search(index + 1, selected, total);
}
}
search(0, [], 0);
return bestSelection;
}
Knapsack
function knapsack(utxos: UTXO[], target: number): UTXO[] {
const n = utxos.length;
const dp: boolean[][] = Array(n + 1).fill(null)
.map(() => Array(target + 1).fill(false));
// Base case: sum of 0 is always achievable
for (let i = 0; i <= n; i++) dp[i][0] = true;
// Fill the table
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= target; j++) {
dp[i][j] = dp[i - 1][j]; // Don't include
if (j >= utxos[i - 1].value) {
dp[i][j] = dp[i][j] || dp[i - 1][j - utxos[i - 1].value];
}
}
}
// Backtrack to find solution
if (!dp[n][target]) return largestFirst(utxos, target); // Fallback
const selected: UTXO[] = [];
let remaining = target;
for (let i = n; i > 0 && remaining > 0; i--) {
if (!dp[i - 1][remaining]) {
selected.push(utxos[i - 1]);
remaining -= utxos[i - 1].value;
}
}
return selected;
}
Fee Estimation
Fee Rate Sources
async function getFeeRate(): Promise<number> {
// Option 1: mempool.space API
const response = await fetch('https://mempool.space/api/v1/fees/recommended');
const fees = await response.json();
return {
fast: fees.fastestFee, // Next block
medium: fees.halfHourFee, // ~30 min
slow: fees.hourFee, // ~1 hour
economy: fees.economyFee, // Low priority
};
}
// Option 2: Bitcoin Core RPC
async function getFeeRateFromNode(rpc: BitcoinRPC, blocks: number): Promise<number> {
const result = await rpc.call('estimatesmartfee', [blocks]);
if (result.feerate) {
// Convert BTC/kB to sat/vB
return Math.ceil(result.feerate * 100000);
}
throw new Error('Fee estimation failed');
}
Calculating Transaction Fee
interface TransactionSizes {
P2PKH_INPUT: 148,
P2WPKH_INPUT: 68,
P2TR_INPUT: 57.5,
P2PKH_OUTPUT: 34,
P2WPKH_OUTPUT: 31,
P2TR_OUTPUT: 43,
OVERHEAD: 10.5,
}
function estimateFee(
inputs: { type: string }[],
outputs: { type: string }[],
feeRate: number
): number {
let vSize = TransactionSizes.OVERHEAD;
for (const input of inputs) {
switch (input.type) {
case 'P2PKH': vSize += TransactionSizes.P2PKH_INPUT; break;
case 'P2WPKH': vSize += TransactionSizes.P2WPKH_INPUT; break;
case 'P2TR': vSize += TransactionSizes.P2TR_INPUT; break;
}
}
for (const output of outputs) {
switch (output.type) {
case 'P2PKH': vSize += TransactionSizes.P2PKH_OUTPUT; break;
case 'P2WPKH': vSize += TransactionSizes.P2WPKH_OUTPUT; break;
case 'P2TR': vSize += TransactionSizes.P2TR_OUTPUT; break;
}
}
return Math.ceil(vSize * feeRate);
}
Replace-By-Fee (RBF)
Enabling RBF
function createRBFTransaction(psbt: Psbt) {
// Set sequence to enable RBF (< 0xFFFFFFFE)
psbt.setInputSequence(0, 0xFFFFFFFD);
// Alternative: Use the constant
psbt.setInputSequence(0, bitcoin.Transaction.DEFAULT_SEQUENCE - 2);
}
Creating Replacement Transaction
function bumpFee(originalTx: Transaction, newFeeRate: number): Psbt {
const psbt = new bitcoin.Psbt();
// Copy inputs from original transaction
for (const input of originalTx.ins) {
psbt.addInput({
hash: input.hash,
index: input.index,
sequence: 0xFFFFFFFD, // RBF enabled
// Add witness UTXO data...
});
}
// Recalculate outputs with higher fee
const originalFee = calculateFee(originalTx);
const newVSize = originalTx.virtualSize();
const newFee = newVSize * newFeeRate;
const additionalFee = newFee - originalFee;
// Reduce change output by additional fee
for (let i = 0; i < originalTx.outs.length; i++) {
const output = originalTx.outs[i];
if (isChangeOutput(output)) {
psbt.addOutput({
script: output.script,
value: output.value - additionalFee,
});
} else {
psbt.addOutput({
script: output.script,
value: output.value,
});
}
}
return psbt;
}
Child-Pays-For-Parent (CPFP)
Creating CPFP Transaction
function createCPFPTransaction(
stuckTx: Transaction,
stuckTxFee: number,
targetFeeRate: number
): Psbt {
// Find our output in the stuck transaction
const ourOutput = findOurOutput(stuckTx);
// Calculate required fee for both transactions
const stuckTxVSize = stuckTx.virtualSize();
const childVSize = 110; // Estimate for simple spend
const totalVSize = stuckTxVSize + childVSize;
const totalFeeNeeded = totalVSize * targetFeeRate;
const childFee = totalFeeNeeded - stuckTxFee;
// Create child transaction
const psbt = new bitcoin.Psbt();
psbt.addInput({
hash: stuckTx.getId(),
index: ourOutput.index,
witnessUtxo: {
script: ourOutput.script,
value: ourOutput.value,
},
});
psbt.addOutput({
address: newAddress,
value: ourOutput.value - childFee,
});
return psbt;
}
Transaction Batching
Batch Multiple Payments
interface Payment {
address: string;
amount: number;
}
function createBatchTransaction(
utxos: UTXO[],
payments: Payment[],
feeRate: number,
changeAddress: string
): Psbt {
const psbt = new bitcoin.Psbt();
// Calculate total needed
const totalPayments = payments.reduce((sum, p) => sum + p.amount, 0);
// Select inputs
const estimatedFee = estimateBatchFee(utxos.length, payments.length + 1, feeRate);
const target = totalPayments + estimatedFee;
const selectedUtxos = selectCoins(utxos, target);
// Add inputs
let totalInput = 0;
for (const utxo of selectedUtxos) {
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
witnessUtxo: {
script: utxo.script,
value: utxo.value,
},
});
totalInput += utxo.value;
}
// Add payment outputs
for (const payment of payments) {
psbt.addOutput({
address: payment.address,
value: payment.amount,
});
}
// Add change output
const actualFee = estimateBatchFee(selectedUtxos.length, payments.length + 1, feeRate);
const change = totalInput - totalPayments - actualFee;
if (change > 546) {
psbt.addOutput({
address: changeAddress,
value: change,
});
}
return psbt;
}
Benefits of Batching
Single transaction to 10 recipients:
- 1 input, 11 outputs (including change)
- ~380 vbytes
- 1 fee payment
10 separate transactions:
- 10 inputs, 20 outputs total
- ~1,100 vbytes total
- 10 fee payments
Time Locks
Absolute Time Lock (nLockTime)
function createTimeLocked(lockTime: number): Psbt {
const psbt = new bitcoin.Psbt();
// Set locktime (block height or Unix timestamp)
psbt.setLocktime(lockTime);
// Must set sequence < 0xFFFFFFFF to enable locktime
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
sequence: 0xFFFFFFFE,
witnessUtxo: {
script: utxo.script,
value: utxo.value,
},
});
psbt.addOutput({
address: recipient,
value: amount,
});
return psbt;
}
// Lock until specific block
createTimeLocked(850000); // Block 850,000
// Lock until specific time (Unix timestamp > 500,000,000)
createTimeLocked(1735689600); // Jan 1, 2025
Relative Time Lock (CSV)
function createCSVLocked(blocks: number): Psbt {
const psbt = new bitcoin.Psbt();
// Set sequence for relative timelock
// Blocks: blocks (up to 65535)
// Time: blocks | 0x00400000 (in 512-second units)
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
sequence: blocks, // e.g., 144 for ~1 day
witnessUtxo: {
script: utxo.script,
value: utxo.value,
},
});
return psbt;
}
Broadcasting Transactions
Error Handling
Common Errors
async function safebroadcast(txHex: string): Promise<string> {
try {
return await broadcastTransaction(txHex);
} catch (error) {
const message = error.message.toLowerCase();
if (message.includes('insufficient fee')) {
throw new Error('Fee too low. Increase fee rate and retry.');
}
if (message.includes('dust')) {
throw new Error('Output amount too small (below dust limit).');
}
if (message.includes('missing inputs') || message.includes('bad-txns-inputs-missingorspent')) {
throw new Error('Input already spent or does not exist.');
}
if (message.includes('txn-mempool-conflict')) {
throw new Error('Conflicting transaction in mempool. May need RBF.');
}
if (message.includes('non-final')) {
throw new Error('Transaction timelock not yet satisfied.');
}
throw error;
}
}
Best Practices
Transaction Construction
- Validate All Inputs: Verify UTXOs exist and are unspent
- Calculate Fees Carefully: Use appropriate fee rate for urgency
- Handle Dust: Don't create outputs below dust limit (~546 sats)
- Use RBF: Enable RBF for flexibility (unless specific reason not to)
- Verify Before Signing: Double-check amounts and addresses
Security
- Test on Testnet: Always test transaction logic on testnet first
- Validate Addresses: Verify recipient addresses are valid
- Check Change Amounts: Ensure change is calculated correctly
- Review Before Broadcast: Final review of all transaction details
Optimization
- Batch When Possible: Combine multiple payments into one transaction
- Use SegWit/Taproot: Lower fees for SegWit and Taproot inputs/outputs
- Consolidate UTXOs: During low-fee periods, consolidate small UTXOs
- Avoid Unnecessary Outputs: Minimize output count when possible
Summary
Transaction construction involves:
- Building: Creating inputs, outputs, and metadata
- Coin Selection: Choosing optimal UTXOs to spend
- Fee Estimation: Calculating appropriate fees
- Signing: Adding valid signatures
- Broadcasting: Submitting to the network
Understanding these fundamentals enables building robust Bitcoin applications that handle funds safely and efficiently.
Related Topics
- PSBT - Partially Signed Bitcoin Transactions
- Coin Selection - UTXO selection algorithms
- Address Generation - Creating recipient addresses
- Mempool - Transaction propagation and fees
