Introduction
Transactions form the core of the Bitcoin network, and the primary purpose of the blockchain is to store these transactions securely and reliably. Once a transaction is created and added to the blockchain, it becomes immutable—no one can alter or delete it. This article begins a two-part series on implementing transactions. In this first part, we will establish the basic framework of a transaction. The second part will delve into more intricate details.
Unlike traditional account-based systems, Bitcoin utilizes an Unspent Transaction Output (UTXO) model. This means there is no direct concept of an "account balance." Instead, balances are derived by scanning the entire history of transactions associated with an address.
The Anatomy of a Bitcoin Transaction
A transaction is composed of inputs and outputs.
type Transaction struct {
ID []byte
Vin []TXInput
Vout []TXOutput
}For each new transaction, its inputs reference (i.e., spend) the outputs of previous transactions (with the exception of the coinbase transaction). Referencing a previous output means including it as part of a new transaction's input. The outputs of a transaction are where the coins are actually stored.
Key points to note:
- Some outputs might not be linked to any input (they are unspent).
- A single transaction input can reference outputs from multiple previous transactions.
- However, every input must reference one specific output.
Throughout this explanation, terms like "money," "spend," "send," and "account" are used for clarity. However, in Bitcoin's protocol, these concepts don't exist in the traditional sense. Transactions simply lock a value with a script, and that value can only be unlocked by the entity that possesses the key to that lock.
Every Bitcoin transaction creates outputs, which are recorded on the blockchain. Sending Bitcoin to someone essentially means creating a new UTXO locked to that person's address, making it spendable by them.
Transaction Outputs (TXOutput)
Let's start by examining the output structure:
type TXOutput struct {
Value int
ScriptPubKey string
}An output consists of two main parts:
- Value: The amount of bitcoin stored.
- ScriptPubKey: A locking script that must be satisfied (unlocked) to spend this output.
The Value field stores the number of satoshis, not whole BTC. One satoshi is one hundred millionth of a BTC (0.00000001 BTC), representing the smallest unit of currency on the Bitcoin network.
The ScriptPubKey defines the conditions that must be met to spend the output. Bitcoin uses a scripting language called Script for this purpose. While we won't delve into its complexities here, it's a purposefully limited language designed for security. For our current implementation, since we haven't yet implemented cryptographic addresses, the ScriptPubKey will simply store an arbitrary user-defined wallet address.
A crucial property of outputs is that they are indivisible. You cannot spend only a portion of an output. If an output's value is greater than the amount you want to send, you must spend the entire output and create a new "change" output that sends the leftover coins back to yourself.
Transaction Inputs (TXInput)
Now, let's look at the input structure:
type TXInput struct {
Txid []byte
Vout int
ScriptSig string
}An input references an output from a previous transaction:
Txid: The ID of the transaction that contains the output we want to spend.Vout: The index of that specific output within the previous transaction's output list (since a transaction can have multiple outputs).ScriptSig: A script that provides data to satisfy the conditions of the output'sScriptPubKey. If the data inScriptSigis correct, the output is unlocked, and its value can be used to create new outputs.
In our simplified model, ScriptSig also stores a user-defined address. This will be replaced with digital signatures and public keys in a future implementation.
In summary, outputs are where coins are stored and locked with a script. Inputs reference these outputs and provide the unlocking data. Every new transaction must have at least one input and one output.
The Coinbase Transaction: The First Output
This leads to a classic "chicken and egg" problem: which comes first, the input or the output? In Bitcoin, the output comes first. The very first transaction in a block has no inputs; it's a special transaction called the coinbase transaction.
This transaction "creates" new coins out of thin air as a reward for the miner who successfully mined the new block. The first block, known as the genesis block, contained the first-ever coinbase transaction. Here's how we can create one:
func NewCoinbaseTX(to, data string) *Transaction {
if data == "" {
data = fmt.Sprintf("Reward to '%s'", to)
}
txin := TXInput{[]byte{}, -1, data}
txout := TXOutput{subsidy, to}
tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
tx.SetID()
return &tx
}A coinbase transaction has a single output and no valid inputs (represented by an empty Txid and a Vout of -1). The ScriptSig can contain arbitrary data. In Bitcoin's actual genesis block, this data included the headline: "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks."
The subsidy is the block reward. In Bitcoin, this reward halves every 210,000 blocks. In our basic implementation, it remains a constant value.
Integrating Transactions into the Blockchain
Now, every block must store at least one transaction. We modify the block structure to replace the generic Data field with a list of transactions:
type Block struct {
Timestamp int64
Transactions []*Transaction
PrevBlockHash []byte
Hash []byte
Nonce int
}The functions for creating new blocks and the genesis block are updated accordingly. The Proof-of-Work algorithm must also be modified to consider the transactions within a block for hashing, ensuring the integrity of the recorded transactions.
func (pow *ProofOfWork) prepareData(nonce int) []byte {
data := bytes.Join(
[][]byte{
pow.block.PrevBlockHash,
pow.block.HashTransactions(), // Now hashes transactions, not data
IntToHex(pow.block.Timestamp),
IntToHex(int64(targetBits)),
IntToHex(int64(nonce)),
},
[]byte{},
)
return data
}The HashTransactions() function creates a unique hash representing all transactions in the block by combining their individual IDs.
Finding Unspent Transaction Outputs (UTXOs)
To check a balance or create a new transaction, we need to find all Unspent Transaction Outputs (UTXOs). A UTXO is an output that has not been referenced as an input in any subsequent transaction.
The process involves iterating through every block and every transaction in the blockchain:
- For each output, check if it can be unlocked by the given address.
- Before counting it, verify that this output has not been spent by checking if it is referenced in any later transaction's input.
- Collect all unspent outputs.
This allows us to calculate the balance for an address by summing the Value of all UTXOs locked to it.
func (cli *CLI) getBalance(address string) {
bc := NewBlockchain(address)
defer bc.db.Close()
balance := 0
UTXOs := bc.FindUTXO(address)
for _, out := range UTXOs {
balance += out.Value
}
fmt.Printf("Balance of '%s': %d\n", address, balance)
}Sending Coins by Creating Transactions
Sending coins involves creating a new transaction. This process requires:
- Finding enough spendable UTXOs from the sender's address to cover the desired amount.
- Creating inputs that reference these UTXOs.
Creating new outputs:
- One output locked to the recipient's address with the sent amount.
- Another "change" output locked back to the sender's address for any leftover value from the inputs.
The function FindSpendableOutputs accumulates UTXOs until their total value meets or exceeds the amount the sender wants to transfer.
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
var inputs []TXInput
var outputs []TXOutput
acc, validOutputs := bc.FindSpendableOutputs(from, amount)
if acc < amount {
log.Panic("ERROR: Not enough funds")
}
// Build inputs from the spendable outputs
for txid, outs := range validOutputs {
txID, err := hex.DecodeString(txid)
for _, out := range outs {
input := TXInput{txID, out, from}
inputs = append(inputs, input)
}
}
// Build outputs
outputs = append(outputs, TXOutput{amount, to}) // Payment to recipient
if acc > amount {
outputs = append(outputs, TXOutput{acc - amount, from}) // Change back to sender
}
tx := Transaction{nil, inputs, outputs}
tx.SetID()
return &tx
}Finally, the send command creates this new transaction and mines a new block to include it in the blockchain.
👉 Explore advanced transaction strategies
Frequently Asked Questions
What is the difference between the UTXO model and the account model?
The UTXO model, used by Bitcoin, treats currency as discrete outputs that are either spent or unspent. It's like using cash. The account model, used by Ethereum and traditional banks, maintains a running balance for each account, which is updated with each transaction.
Why does a transaction input need to reference a previous output?
This reference provides proof that the spender has the right to use the coins. It creates an auditable chain of ownership, linking all the way back to the coins that were originally mined.
What happens if I try to spend more than I have?
The transaction creation process will fail. The code checks if the accumulated value of available UTXOs is sufficient before building the transaction. If not, it panics with an "ERROR: Not enough funds" message.
What is the purpose of the change output?
Since UTXOs are indivisible, you must spend entire outputs. If the value of the UTXOs you use is greater than the amount you want to send, a change output is created to return the leftover funds to your address, preventing overpayment.
Is storing entire transactions in a block efficient?
For simplicity, our implementation hashes all transaction IDs together. However, Bitcoin uses a more efficient structure called a Merkle Tree. This allows for quick verification of whether a transaction is included in a block without downloading the entire block.
What key features are still missing from this basic implementation?
This implementation lacks several critical features: cryptographic addresses and signatures, a dynamic block reward, a dedicated UTXO set for faster querying, and a memory pool (mempool) to hold transactions before they are mined.