Quick Answer

A blockchain is a chain of blocks where each block stores its data plus the cryptographic hash of the block before it, so changing any past block breaks every hash that comes after it. Miners compete to find a number (a nonce) that makes a block's hash start with a target number of zeros, a deliberately slow puzzle called proof-of-work. We built a real one in Python below, mined it, and show the actual hashes and attempt counts, no hand-waving.

What a block actually contains

Strip away the marketing and a blockchain is a very ordinary data structure: a linked list where each node (called a block) points to the previous one not with a memory address, but with a cryptographic hash of that previous block's entire contents. That one design choice is the whole trick, and everything else in this post is a consequence of it.

A minimal block needs five fields: an index (its position in the chain), a timestamp, the actual data it's carrying (in Bitcoin this is a list of transactions; it can be anything), the prev_hash of the block before it, and its own hash, which is computed from everything else including prev_hash. Here's the actual class we'll mine with in this post:

class Block:
    def __init__(self, index, timestamp, data, prev_hash):
        self.index = index
        self.timestamp = timestamp
        self.data = data
        self.prev_hash = prev_hash
        self.nonce = 0
        self.hash = self.compute_hash()

    def compute_hash(self):
        block_string = json.dumps({
            "index": self.index,
            "timestamp": self.timestamp,
            "data": self.data,
            "prev_hash": self.prev_hash,
            "nonce": self.nonce,
        }, sort_keys=True)
        return hashlib.sha256(block_string.encode()).hexdigest()

Notice that hash is not an input you choose, it's an output computed from everything else, including a field called nonce we haven't explained yet. That's the mining puzzle, and it's the next section.

Why SHA-256 is the load-bearing piece

A cryptographic hash function like SHA-256 takes an input of any size and produces a fixed-size output (256 bits, shown as 64 hexadecimal characters) with three properties that matter here: it's deterministic (same input always gives the same output), it's effectively one-way (you can't work backward from the hash to the input), and it has an avalanche effect (changing a single bit of input scrambles the entire output unpredictably).

That avalanche effect is what makes tampering detectable. If a block's hash depends on every field in it, then editing the data even slightly, say changing a transaction amount from 20 to 999999, produces a completely different hash with no visible relationship to the original. There's no way to nudge the data and get a "close enough" hash. We'll prove this with real numbers in a few sections.

Run the compute_hash function above on a genesis block once, with nonce=0, and you get one specific 64-character hex string, deterministically, every single time you run it on that exact input. That determinism is what lets every node in a network independently verify a block without trusting anyone: recompute the hash yourself, compare it to what's claimed, done.

Mining: the deliberately slow puzzle

Here's where the nonce field earns its keep. Proof-of-work requires a block's hash to satisfy an arbitrary condition: for this post, the hash must start with a set number of zero characters (the difficulty). Since you can't predict what a hash will look like without computing it (that's the whole point of a one-way function), the only strategy is brute force: try nonce=0, hash it, check the prefix, if it fails try nonce=1, and so on.

def mine(self, difficulty):
    target = "0" * difficulty
    attempts = 0
    while not self.hash.startswith(target):
        self.nonce += 1
        attempts += 1
        self.hash = self.compute_hash()
    return attempts

We actually ran this. Mining four linked blocks at difficulty 4 (hash must start with 0000) on ordinary hardware gave us these real results, nonce and hash both straight from the terminal, nothing edited:

Block 0 (genesis): nonce=37300  attempts=37300
  hash: 00005ef15b1e97e4afc59623f7ef6354933700651187893e3a70b2c8449c5f16
Block 1: nonce=184788 attempts=184788
  prev_hash: 00005ef15b1e97e4afc59623f7ef6354933700651187893e3a70b2c8449c5f16
  hash:      00007f6c4ca2ba7e964fed23cbb7fc795e53a3d4b14cae54872724c6e7a30d08
Block 2: nonce=33932  attempts=33932
  prev_hash: 00007f6c4ca2ba7e964fed23cbb7fc795e53a3d4b14cae54872724c6e7a30d08
  hash:      0000f1555699137cf93c91b7a06af22f50a1e7e57f794b9204feb7ccb6b788ae
Block 3: nonce=13873  attempts=13873
  prev_hash: 0000f1555699137cf93c91b7a06af22f50a1e7e57f794b9204feb7ccb6b788ae
  hash:      000056fd90050840dae95dc025e660e18d0df076724baf83bac47c2f81326ee1

A few things worth noticing in real output like this. First, every block's hash starts with 0000, that's difficulty 4 satisfied. Second, every block's prev_hash is exactly equal to the previous block's hash, character for character, that's the chain link. Third, the nonce needed varies wildly between blocks (37300, then 184788, then 33932, then 13873) because each block's starting data is different, so the search is a completely independent random walk each time. There is no shortcut and no pattern to exploit, that randomness is the point.

Verification of a mined block, by contrast, is instant: one SHA-256 computation to check the hash matches, one string comparison to check the prefix. This asymmetry (expensive to produce, cheap to check) is exactly why proof-of-work works as a trust mechanism: nobody has to trust the miner's honesty, they just recompute and check.

What actually happens when you tamper with a block

This is the part usually described in the abstract ("tampering breaks the chain") without anyone showing what "breaks" actually looks like in bytes. So we tampered with our own mined chain and captured the real result. We took Block 2, whose real data was "Block 2 data: transfer 20 coins", and changed it in place to "Block 2 data: transfer 999999 coins (TAMPERED)", without re-mining anything, exactly what an attacker trying to rewrite history would attempt.

Block's stored hash (unchanged):      0000f1555699137cf93c91b7a06af22f50a1e7e57f794b9204feb7ccb6b788ae
Hash recomputed from tampered fields: 7500d212d7501d8c03c2dbb83b245502761e25c07b327de05fd831f06f16a965
Do they match? False

The block's stored hash field still says 0000f155..., because we didn't touch that field, we only touched data. But the moment any validator recomputes the hash from the block's current contents, they get 7500d212..., a completely different string with zero leading zeros, meeting neither the original difficulty target nor matching the stored value. The mismatch is immediate and total, exactly the avalanche effect from earlier.

Now the interesting question: what if the attacker is smarter and also overwrites the stored hash field to hide the tampering, and even re-mines it to satisfy difficulty 4 again? We simulated that too, by patching Block 2's hash to the freshly recomputed value. The block now looks internally consistent, its own hash matches its own contents. But Block 3's prev_hash field still points to the old value:

After patching Block 2's own hash field: Chain is INVALID
  Block 3's prev_hash still points to the OLD Block 2 hash:
    0000f1555699137cf93c91b7a06af22f50a1e7e57f794b9204feb7ccb6b788ae
  but Block 2's hash is now:
    7500d212d7501d8c03c2dbb83b245502761e25c07b327de05fd831f06f16a965

This is why the linking matters more than any individual block's internal validity: to rewrite Block 2 undetected, the attacker has to also update Block 3's prev_hash, which changes Block 3's own hash, which then breaks Block 4's prev_hash, and so on to the end of the chain. Rewriting history means re-mining every single block from the tampered one forward, redoing all that proof-of-work, faster than the rest of the network is adding new blocks on top. On a real network with thousands of independent miners, that's the actual security guarantee, not "it's a blockchain so it's secure" but "catching up requires more computing power than everyone else combined, continuously."

Difficulty scaling: why one more zero costs 16x more work

SHA-256 output is hexadecimal, so each character is one of 16 possible values (0-9, a-f). Requiring one specific leading character to be zero has roughly a 1-in-16 chance per attempt; requiring two leading zeros drops that to roughly 1-in-256 (16 squared); requiring d leading zeros gives roughly a 1-in-16^d chance per attempt. That's why difficulty is measured in leading zero characters, and why each additional zero multiplies the expected work by about 16x.

We measured this directly by mining fresh, independent blocks at difficulties 1 through 5 and counting real attempts:

difficulty=1: attempts=4        hash=0d242bb5def56bfb07b9fb3dd653212637e98e20f77690d624a048057cd8ca00
difficulty=2: attempts=53       hash=00b6af8d4cd58c6573e788aea17fd00bf2dd9b6eefc175e702ef5483992fecfe
difficulty=3: attempts=8166     hash=000b886622a113d02c9b1447f3dba6f5d6494bfb4d86cade3b1e75975c335667
difficulty=4: attempts=22226    hash=0000d46bf0e797b82120235270bd0fdf1304d0dcf4bb0c71aabfb4a724d3c584
difficulty=5: attempts=3480006  hash=0000035fcc7a19f460f480c66b996cbd8fcd7c89d51c3b9bdf9f19ad58358cba

These are single runs, and mining is a random search, so any individual run can land far from the theoretical average (compare this difficulty-4 run's 22,226 attempts to the 184,788 attempts one of our chain blocks needed earlier, same difficulty, wildly different luck). What doesn't vary is the trend: going from difficulty 3 to difficulty 5 took roughly 426x more attempts (8,166 to 3,480,006), consistent with the expected 16^2 = 256x scaling for two more zeros, within the noise you'd expect from a random process. This is exactly the lever real networks use: Bitcoin recalculates its difficulty target every 2,016 blocks specifically to keep average block time near 10 minutes as total network computing power changes, more miners joining means higher difficulty, not faster blocks.

Real blockchains also don't hash a single "data" string, they hash a Merkle tree root, a binary tree of hashes built from potentially thousands of individual transactions, so that a single 32-byte hash in the block header effectively fingerprints an entire batch of transactions, and any single transaction can be proven to be included in the block without transmitting all the others. The mining loop itself, though, works exactly like the toy version here, just against that Merkle root instead of a single data string.

Consensus: why the network agrees on one history

A blockchain only matters if many independent nodes, who don't trust each other, can agree on a single shared history without a central authority. Proof-of-work solves this with a simple rule: whichever valid chain represents the most cumulative computational work is the one everyone follows. This is usually called Nakamoto consensus.

If two miners solve a block at nearly the same time, the network temporarily forks, some nodes see one block first, some see the other. Both are valid on their own. The fork resolves itself naturally: whichever branch gets the next block first now represents more total work, and rational miners abandon the shorter branch and start building on the longer one, because a shorter chain will never catch up in total work unless its miners control the majority of the network's hash power. That's also the well-known "51% attack": if one actor genuinely controls more than half the network's total mining power, they can, in principle, out-mine everyone else and force their own version of history to win. It's not a bug in the logic, it's the explicit, publicly known assumption the entire system rests on: that no single actor controls a majority of the resource being spent.

This is also the core trade-off proof-of-work makes: security is bought with real, ongoing energy expenditure, which is precisely why alternatives like proof-of-stake exist (used by Ethereum since 2022), replacing "prove you burned electricity" with "prove you've locked up a financial stake you'd lose by cheating." The consensus goal is identical, only the cost mechanism differs. Whichever mechanism is used, the underlying data structure you saw in this post, blocks linked by hashes of their contents, mined against a difficulty target, tamper-evident by construction, is the same.

Frequently Asked Questions

Is a blockchain the same thing as Bitcoin? No. Bitcoin is one application built on a blockchain. The blockchain itself is just the data structure (hash-linked blocks with proof-of-work); it can store anything, not only cryptocurrency transactions, and plenty of non-financial systems use the same idea.
Why can't you just fix a tampered hash and move on? Because each block's hash is derived from the previous block's hash. Fixing one block's hash to hide tampering changes that block's identity, which breaks the next block's stored reference to it, cascading forward through every subsequent block, which all have to be re-mined too.
Does mining actually solve anything useful, like a real math problem? No, and that's intentional. The puzzle (find a nonce that produces a hash with N leading zeros) has no useful output beyond proving computational effort was spent. That's exactly why it works as an anti-cheating mechanism: the work is real and verifiable, but arbitrary.
What stops someone from just editing the difficulty requirement? Every node independently checks the difficulty rule and rejects blocks that don't satisfy it, regardless of what the block's creator claims. A block that doesn't meet the network's current difficulty target is simply invalid and gets ignored by honest nodes, no negotiation involved.
Why does Bitcoin use 10-minute blocks instead of something faster? It's a deliberate trade-off: slower blocks give more time for a new block to propagate to every node worldwide before the next one is found, reducing accidental forks. Faster blockchains exist but accept more frequent forking or use different consensus mechanisms entirely.