How do Bitcoin Transactions actually work?

Leonardo Bronstein Franceschetti
ProFUSION Engineering
10 min readJun 29, 2023

Introduction

We’ve probably all heard of Bitcoin at one point in our lives, and some of us may or may not have looked further into the rabbit hole that is the world founded by Satoshi Nakamoto. If you need some refreshing, here it is:

Bitcoin is a decentralized cryptocurrency native to the internet. The technology works by having countless people running the same program all over the globe (these are called nodes) which can accept transaction requests from Users, verify that the transaction is valid and, if so, relay that transaction to miners, who will try to organize lots of transactions into blocks that will be recorded and set in stone. These blocks are stored in a Blockchain, where each block references the previous one to make sure that they can't be reordered or altered.

But how do the nodes know if a requested transaction is valid?

Each node keeps its own copy of the Blockchain, so they all have the complete record of all transactions that have ever happened in the network, since its inception. Thus, they can make sure that no one is creating new coins out of thin air or trying to bypass some network security.

Bitcoins are stored in what are called wallets. These wallets have various addresses and a certain amount of coins is in each address. To send someone some money, you create a transaction to one of that person's addresses. It seems pretty simple, right?

Well, let’s dive further into the core of these data structures.

The Structure of a Bitcoin Transaction

A transaction is very lightweight, and it contains the following metadata (redacted):

  • Version
  • Number of inputs (where coins will come from)
  • List of inputs
    – Previous transaction
    – Index (which output to use from the previous transaction)
    ScriptSig (script used to unlock the coins from the output above)
  • Number of outputs (where coins will go)
  • List of outputs
    – Value (the amount of coins coming here)
    ScriptPubKey (the script that will be used to unlock the coins from this output)

Note: some additional metadata was omitted because it’s not relevant to this discussion and would add unnecessary complexity

You may have noticed a couple of things like: Inputs simply reference a "previous output"; the so-called "Script" is what dictates how outputs are unlocked so that they can be spent and there's no mention of any kind of address in the transaction data. Weird, huh? Let's go step-by-step.

The UTXO

Bitcoin as a protocol implements the UTXO standard, which stands for Unspent Transaction Output. In the transaction structure we just looked at above, the UTXOs are just the list of outputs, but only when they have NOT been referenced by any input in another transaction. As you can see, a UTXO is very simple, it has a value - a number of coins - and a ScriptPubKey - script used to unlock the coins when solved correctly.

The Input

Now that you know how a UTXO works, you can understand the inputs a little better. They contain a reference to a UTXO (Previous transaction + index) and a ScriptSig, which, in simple terms, is a value that will be processed by the ScriptPubKey. If no step in the script triggers a failure and the result is true (non-zero), the coins will be unlocked and will be spendable by the input.

What actually happens is that the nodes will take the ScriptSig value, append the ScriptPubKey to it, and process the final result as one big script. This behavior is possible because the Script language is stack-based (like Forth)). More on this later.

Why are there no mentions of addresses in the transactions, you ask? That’s because there are actually different types of addresses which contain different data used to create different types of scripts for UTXOs to be unlocked. Addresses are all just conventions and they're basically independent of the actual transaction data. Some addresses are just a public key (which has its private key counterpart), some are the hash of that public key, and on and on. When you send an address to someone, you send the data associated with that address type and the person sending you Bitcoins will use that data to create a script most suitable for it. For example:

You create a new random private key, and you hash its public key counterpart. That is your new address, and you send someone that address (the hash of your public key). A script to unlock that UTXO may ask for the public key of the owner of the address (you), and, when processed (by someone trying to spend that UTXO by providing a public key), the script will hash that public key; if it matches the hash you gave them at the beginning (your address), that means you have unlocked the script and the coins can be spent (this is a common script format called P2PKH — Pay To Public Key Hash — but we’re getting ahead of ourselves).

Does this mean that a transaction can contain ANY kind of script, and it’s not tied to any particular convention regarding addresses and such? Yes! If you want, you can create a transaction that says that, to spend the coins, you need to send the value “potato beans”. This isn’t very safe though, so I wouldn’t recommend it ºuº

Bitcoin Script

The Bitcoin Script, also known as Bitcoin Scripting Language and Script, is a language created specifically for the BTC network. It's Forth) -like, stack-based and processed from left to right. It's incredibly simple, but lots of cool things (smart contracts) can be done with it.

The language has what are called Opcodes (Operation codes). These are operators that manipulate the stack/run functions. There are simple ones, like OP_ADD, and more complex ones, like OP_HASH160 (which will perform a hash on the current value in the stack). You can take a look at a list containing all of the Opcodes here.

In Bitcoin transactions, as mentioned earlier, there are two script components:

Script Components of a Transaction

Every transaction has two script parts associated with them:

  • ScriptPubKey
  • ScriptSig

ScriptPubKeys are in the UTXOs, and ScriptSigs unlock them. How, exactly? They're concatenated. This means that, at the time of processing transactions, a node will put them next to each other and treat them as one big script, then run it.

Let’s take a look at one of the most common scripts used in transactions, the P2PKH script mentioned above:

Pay-To-Pub-Key-Hash

This is a very common script, which I exemplified earlier. Here it is in its entirety:

ScriptPubKey

OP_DUP OP_HASH160 <pub-key-hash> OP_EQUALVERIFY OP_CHECKSIG

ScriptSig

<pub-key> <signature>

The signature is performed with the Private Key on top of the Public Key. It returns a value that can be verified to ensure that the person that sent the Public Key does, in fact, have the value of the Private Key, but without exposing the Private Key itself. Without this, anyone with the Public Key would be able to spend the coins.

Remember, ScriptSig is what you send when you want to spend the coins. The concatenated Script will look like this:

<signature> <pub-key> OP_DUP OP_HASH160 <pub-key-hash> OP_EQUALVERIFY OP_CHECKSIG

Alright, so these are the exact steps performed by the Script processor:

  • Signature is pushed onto the Stack
  • PubKey is pushed onto the Stack
  • OP_DUP is executed
    – This operation pops the last value on the Stack (PubKey), duplicates it and pushes both values back onto the Stack
    – After the operation, the Stack looks like this:
    Signature
    PubKey
    PubKey
  • OP_HASH160 is executed
    – This operation pops the last value on the Stack (PubKey), hashes it with different algorithms and then pushes the resulting hash onto the Stack
    – After the operation, the Stack looks like this:
    Signature
    PubKey
    PubKeyHash
  • PubKeyHash is pushed onto the Stack
  • OP_CHECKEQUALVERIFY is executed
    – This operation pops the last 2 values on the Stack (the PubKeyHash calculated by the script and the PubKeyHash that was already in the ScriptPubKey), verifies their equality and exits if they don't match
    – After the operation (assuming success), the Stack looks like this:
    Signature
    PubKey
  • OP_CHECKSIG is executed
    – This operation pops the last 2 values on the Stack (PubKey and Signature) and performs calculations to verify that the Signature was, indeed, created by the owner of that PubKey's private counterpart. If successful, it pushes 1 onto the Stack
    – After the operation (assuming success), the Stack looks like this:
    1

Voilá, the coins have been unlocked!

We could talk about some other script types like P2PK, but there's one in specific that is very important and somewhat unorthodox when compared to the other ones. Let's take a look.

Pay-To-Script-Hash

This may seem somewhat contradictory. How can we embed the Script hash in itself? Well, the reason as to why this is so important is because it allows the RECEIVER of the coins to specify the spending conditions, which isn’t generally the case in the other ones. Regular addresses send info to the sender and the SENDER creates a script around that info. This is not the case with P2SH scripts.

In P2SH, the address doesn't contain info like keys. The address for this script actually contains the hash of a ScriptSig, so when you're sending this address to someone (because you want to receive some coins), you're already saying "This is the ScriptSig that will be used to unlock the UTXO". So the ScriptPubKey created by the SENDER will just receive a serialized Bitcoin Script, hash it, and see if it matches the hash it got in the beginning (the address the RECEIVER sent). If it does match, it'll then deserialize it and run it.

This means that this UTXO can have arbitrarily complex spending conditions! But didn't I say at the beginning that the transactions can contain any sort of spending condition either way? Okay, let's back up for a second. Something I didn't mention is that there is such a thing as "Standard" Bitcoin Transactions. While transactions CAN contain ANY kind of script, a lot of validation nodes block any script patterns that they don't recognize, so the "potato beans" example I gave before might get initially rejected by some nodes or miners (but if a miner does accept and mine them, they're locked in stone). With P2PSH, however, this is not the case. If you know anything about hashes, you know that they're one-way operations. It's not possible to take a hash and find out the value that was hashed to get it.

When you send the hash of your script in a form of a P2SHP address, you're hiding your script completely, the sender doesn't know any details about the spending conditions. This makes a number of complex scripts and smart contracts easier to store on the Blockchain, since they can be infinitely long and still take up the same amount of space, because hashes are constant in length. It's the standard way to create multisig wallets, which allows the Lightning Network to work, and it was a major breakthrough in the history of Bitcoin.

The Lightning Network

Well, now you know roughly how Bitcoin transactions work. And I bet it's way cooler than you initially thought. Raw Blockchain transactions aren't the only way to spend Bitcoins, though, there are Layer 2 solutions that can facilitate micro-payments with cheap and fast processing. The most famous one is the Lightning Network, since it's the most decentralized one and with the most support.

Layer 2 solutions aren’t stored on the Blockchain, which may seem weird, since that’s the whole purpose of Bitcoin. Well, these micro-payments allow us to have more privacy (since they're not on public ledgers), faster transactions (faster than VISA cards, believe it or not) and lower fees (sometimes no fees at all). The most important aspect of this is that they're rooted on the security of the Blockchain, they're glued to Layer 1. There are some really smart smart contracts (haha) at play here which allows these off-chain transactions to actually be REAL Bitcoin transactions.

I won’t go into more detail here because this isn’t the point of this article, but I strongly recommend you read the white paper, which explains everything very thoroughly (and quite honestly, it’s way beyond my capabilities of relaying information, I would never be able to explain it as well as the creators did). It’s a very long document, though (a lot longer than the Bitcoin white paper), so you may want to take a look at the sections that interest you the most. They go in-depth into how so-called channels work and how they allow for such fast, cheap, secure and decentralized transactions.

VERY IMPORTANT: Keep in mind that just because a Layer 2 solution is rooted in the Blockchain, therefore it can generally be considered secure, it DOES NOT mean that it is centralized (although this IS the case with LN). There are centralized Layer 2 solutions, which are still interesting to read about, but they go against a core philosophy of the Bitcoin network and ecosystem.

Conclusion

The engineering behind this cryptocurrency is probably a lot different and more complex than you might’ve anticipated. This is something that is, unfortunately, used by many bad actors in the “crypto world” (ugh) to create scam coins, trick people into buying them and trusting (not verifying!) their security, pump the price and rug pull it, stealing all of the money to themselves (and killing the coin in the process).

Always remember that, when dealing with cryptocurrencies, your money is at real stake. If done well, it can be an amazing piece of software engineering with the potential to change the world (like in Bitcoin). But you must ALWAYS do your own research BEFORE spending any money on things like this. This was an educational article which aims to help more tech-savvy individuals understand how this truly works, without metaphors or analogies. Analogies never paint a full picture. This is NOT financial advice, it is technical education!

Don’t hesitate to search for other sources on everything you learned here, read more about it and, who knows, if you’re a programmer you might even get some cool insight by looking at the actual source code, it’s definitely very interesting to read (although, TW, it’s written in C++!).

What did you think of this article? Did you learn something new? Did I get anything wrong? Feel free to comment below!

Sources

--

--