DEV Community

Cover image for Understanding Bitcoin Transactions: A Developer's Guide
Krrish Verma
Krrish Verma

Posted on

Understanding Bitcoin Transactions: A Developer's Guide

Introduction:-
As a developer exploring Bitcoin for the first time, I found that understanding transactions was the key to unlocking how the entire system works. Unlike traditional payment systems where a central database updates account balances, Bitcoin uses a fundamentally different model: the UTXO (Unspent Transaction Output) system.
In this guide, I'll break down Bitcoin transactions from a developer's perspective, explain how UTXOs work, and show you how to read real transaction data. Whether you're new to Bitcoin or coming from web development (like me), this post will give you a solid technical foundation.

What is a Bitcoin Transaction?
At its core, a Bitcoin transaction is a data structure that transfers value from one address to another. But unlike a simple "send $10 from Alice to Bob" entry in a database, Bitcoin transactions are:

Immutable - Once confirmed, they can't be changed
Publicly verifiable - Anyone can verify the transaction's validity
Cryptographically signed - Only the owner of the private key can spend their bitcoin

Here's what a simplified transaction looks like conceptually:
Transaction {
inputs: [previous_outputs_being_spent],
outputs: [new_outputs_being_created],
signatures: [cryptographic_proofs]
}

The UTXO Model: Bitcoin's Accounting System
Bitcoin doesn't track "account balances" like your bank does. Instead, it uses UTXOs (Unspent Transaction Outputs).
Think of UTXOs like cash bills:

You receive a $50 bill (UTXO worth 0.5 BTC)
You want to buy something for $30
You give the $50 bill (spend the entire UTXO)
You receive $20 back as change (new UTXO worth 0.2 BTC)

Key insight: You can't spend "part" of a UTXO. You must spend it entirely and create change outputs if needed.
Example:

Simplified UTXO representation

utxo_1 = {
"tx_id": "abc123...",
"output_index": 0,
"amount": 0.5, # BTC
"address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
}

When you spend this UTXO, you create a transaction:

transaction = {
"inputs": [
{
"previous_tx": "abc123...",
"output_index": 0,
"signature": "valid_signature_here"
}
],
"outputs": [
{
"amount": 0.3, # BTC to recipient
"address": "recipient_address"
},
{
"amount": 0.199, # BTC change back to you
"address": "your_change_address"
}
# Note: 0.001 BTC goes to miners as fee
]
}

Anatomy of a Real Bitcoin Transaction
Let's break down an actual transaction structure. Here's what the data looks like:
{
"txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9",
"vout": 0,
"scriptSig": {
"asm": "304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901",
"hex": "47304402204e45e..."
},
"sequence": 4294967295
}
],
"vout": [
{
"value": 10.00000000,
"n": 0,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 62e907b15cbf27d5425399ebf6f0fb50ebb88f18 OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac",
"address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
}
},
{
"value": 40.00000000,
"n": 1,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 ...",
"address": "change_address_here"
}
}
]
}

Key Components:

  1. txid: Unique identifier (hash of the transaction)
  2. vin (inputs): References to previous UTXOs being spent
  3. vout (outputs): New UTXOs being created
  4. scriptSig: Proof you own the input (signature)
  5. scriptPubKey: Lock on the output (defines who can spend it)

How Transaction Validation Works
When a node receives a transaction, it validates:

Signatures are valid - Proves the sender owns the inputs
Inputs exist and are unspent - No double-spending
Input amounts ≥ Output amounts - Conservation of value (difference = miner fee)
Scripts execute successfully - Locking/unlocking scripts work

Here's simplified validation pseudocode:
def validate_transaction(tx):
total_input = 0
total_output = 0

# Check all inputs
for input in tx.inputs:
    # Verify the input exists and is unspent
    utxo = get_utxo(input.previous_tx, input.output_index)
    if utxo is None or utxo.is_spent:
        return False

    # Verify signature
    if not verify_signature(input.signature, utxo.scriptPubKey):
        return False

    total_input += utxo.amount

# Check all outputs
for output in tx.outputs:
    if output.amount <= 0:
        return False
    total_output += output.amount

# Ensure inputs >= outputs (difference is miner fee)
if total_input < total_output:
    return False

return True
Enter fullscreen mode Exit fullscreen mode

---

## Transaction Fees: Why They Matter

The difference between input and output amounts goes to miners:
Enter fullscreen mode Exit fullscreen mode

Fee = Sum(inputs) - Sum(outputs)

Example:

Input: 1.0 BTC
Output 1: 0.4 BTC (to recipient)
Output 2: 0.599 BTC (change to yourself)
Fee: 0.001 BTC (goes to miner)

Why fees matter:

Miners prioritize higher-fee transactions
Fees vary based on network congestion
You set the fee when creating a transaction

Practical Example: Reading a Transaction
Let's look at a real transaction on a block explorer:
Transaction ID: f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16
This is actually the first-ever Bitcoin transaction (Satoshi sending to Hal Finney).
What happened:

Satoshi spent a UTXO from a previous block reward
Created an output sending 10 BTC to Hal
Created a change output back to himself
Included a small miner fee

You can view this on any block explorer like:

blockchain.com/explorer
blockstream.info
mempool.space

Building a Simple Transaction (Conceptual)
Here's how you'd construct a transaction programmatically:

from bitcoin import SelectParams
from bitcoin.core import COIN, COutPoint, CMutableTxIn, CMutableTxOut, CMutableTransaction
from bitcoin.wallet import CBitcoinAddress, CBitcoinSecret

Select network (testnet for development)

SelectParams('testnet')

Your private key (keep this secret!)

private_key = CBitcoinSecret('cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy')

Previous transaction you want to spend

txid = "previous_transaction_id"
vout = 0 # Which output of that transaction

Create input (spending the UTXO)

txin = CMutableTxIn(COutPoint(lx(txid), vout))

Create outputs

recipient_address = CBitcoinAddress('recipient_address_here')
change_address = CBitcoinAddress('your_change_address')

txout_recipient = CMutableTxOut(0.01 * COIN, recipient_address.to_scriptPubKey())
txout_change = CMutableTxOut(0.0099 * COIN, change_address.to_scriptPubKey())

0.0001 BTC fee implicit

Create transaction

tx = CMutableTransaction([txin], [txout_recipient, txout_change])

Sign transaction (simplified - actual signing is more complex)

signature = sign_transaction(tx, private_key)

tx.vin[0].scriptSig = signature

Broadcast to network

broadcast_transaction(tx)


**Note:** This is simplified pseudocode. Real Bitcoin transaction creation requires careful handling of scripts, signatures, and encoding.

---

## Common Transaction Types

### 1. **P2PKH (Pay-to-Public-Key-Hash)**
Most common. Sends to a Bitcoin address.
Enter fullscreen mode Exit fullscreen mode

scriptPubKey: OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG


### 2. **P2SH (Pay-to-Script-Hash)**
Sends to a script hash (used for multisig, SegWit).
Enter fullscreen mode Exit fullscreen mode

scriptPubKey: OP_HASH160 OP_EQUAL


### 3. **Multisig**
Requires multiple signatures (e.g., 2-of-3).
Enter fullscreen mode Exit fullscreen mode

scriptPubKey: 2 3 OP_CHECKMULTISIG




---

## Key Takeaways

1. **Bitcoin uses UTXOs, not account balances** - Think of them like cash bills
2. **Transactions spend entire UTXOs** - Change is returned as a new UTXO
3. **Every transaction is validated** - Signatures, amounts, scripts must all check out
4. **Fees incentivize miners** - The difference between inputs and outputs
5. **Everything is public** - But addresses don't inherently reveal identity

---

## What I'd Change If I Rewrote This Today

This post gives a solid foundation, but if I were to expand it, I would:

1. **Add SegWit explanation** - How witness data separates from transaction data
2. **Include Taproot** - The latest upgrade to Bitcoin's scripting
3. **Show mempool dynamics** - How unconfirmed transactions wait for mining
4. **More code examples** - Actual working Python scripts using `python-bitcoinlib`
5. **Visual diagrams** - Transaction graphs showing input/output relationships
6. **Cover RBF (Replace-By-Fee)** - How to update unconfirmed transactions

---

## Resources to Learn More

- **Bitcoin Developer Guide:** https://developer.bitcoin.org/
- **Mastering Bitcoin (Book):** By Andreas Antonopoulos
- **Bitcoin Optech:** https://bitcoinops.org/
- **Bitcoin Core Source Code:** https://github.com/bitcoin/bitcoin
- **Learn Me a Bitcoin:** https://learnmeabitcoin.com/

---

## Conclusion

Understanding Bitcoin transactions is the first step to becoming a Bitcoin developer. The UTXO model might seem strange at first, especially if you're used to traditional databases, but it's what makes Bitcoin's security and verifiability possible.

As I continue my journey into Bitcoin development (currently applying to Summer of Bitcoin 2026), I'm realizing that every part of the protocol—from consensus to cryptography—is designed around making transactions trustless and verifiable by anyone.

If you found this helpful, let me know! I'm documenting my Bitcoin learning journey and would love to hear what topics you'd like to see covered next.

**Follow me for more Bitcoin development content!**

---

#bitcoin #blockchain #cryptocurrency #webdev #tutorial

---
Enter fullscreen mode Exit fullscreen mode

Top comments (0)