Part I · Bricks: the cryptographic floor By the end of this chapter you will have a function that fingerprints any data, and a guessing game — built out of that function — that nobody can cheat at.
Cryptography is, from beginning to end, an argument about bytes. So before touching a single big word, spend ten minutes getting comfortable with something small: the same thing can be written down several different ways.
One thread runs under this whole chapter, and the next seventeen keep returning to it:
representation → one-wayness → composition
1.0 Four costumes, one thing¶
Here is a string, and here is what a computer actually has when it holds one.
# A short string
s = "café42"
print("string :", s)
# encode() turns characters into bytes. In UTF-8 not every character is one byte.
b = s.encode("utf-8")
print("bytes :", b)
print("byte count :", len(b), "bytes")
# bytes -> hex: each byte becomes two hex characters
print("hex :", b.hex())
# bytes -> one big integer
n = int.from_bytes(b, "big")
print("as integer :", n)
string : café42
bytes : b'caf\xc3\xa942'
byte count : 7 bytes
hex : 636166c3a93432
as integer : 27973116691559474
Four lines of output, and they are four costumes on one body:
café42— the way you read itb'caf\xc3\xa942'— what is actually in memory: 7 bytes for 6 characters636166c3a93432— those same 7 bytes, two hex digits each, so 14 characters27973116691559474— all 7 bytes read as a single binary number
Notice the 7. Six characters, seven bytes. The é takes two. Text is not a sequence of
letters as far as a computer is concerned; it is a sequence of bytes, and how many bytes a
character costs depends on the encoding. This trips people up constantly, and it is the first
thing that has to go.
Do not think of hex as “another encoding.” It is the same bytes, printed differently — the way Celsius and Fahrenheit are the same temperature. Every TXID, every address, every public key you have ever seen on a block explorer works this way: bytes underneath, hex on the screen.
That is idea number one: representation. Change the costume, the body is unchanged.
YOUR TURN¶
# YOUR TURN:
# 1) Change s to your own name and rerun. Watch the byte count.
# 2) Go backwards: given a hex string, recover the text.
hexstr = "636166c300a93432" # not the value above — I broke one byte on purpose
recovered = bytes.fromhex(hexstr) # hex -> bytes
# print(recovered.decode("utf-8")) # uncomment this — it may raise. Why?
print("bytes:", recovered)
bytes: b'caf\xc3\x00\xa942'
The exception here is the content, not a bug. Uncomment the decode() and you will
almost certainly get a UnicodeDecodeError, because I zeroed out a byte in the middle of
that é.
Which gives us the chapter’s first real conclusion:
Not every string of bytes is valid text. But every string of bytes can be hashed.
A hash does not care whether you feed it English, Chinese, a JPEG, or corrupted garbage. It only sees bytes. That indifference is exactly why it can be used everywhere later.
1.1 Hashing: easy in, impossible out¶
Now give the idea a picture.
A blender. Put an apple in, press the button, get juice. Nobody staring at the juice can reconstruct the apple — how big it was, how many seeds. But blend the same apple again and you get the same juice.
Apple → juice: trivial. Juice → apple: impossible. That is what “one-way” means.
A fingerprint. Billions of people, no two prints alike. Hand someone a print and they cannot tell you who it belongs to; bring them a suspect and they can tell you instantly whether it matches. Hashing takes a fingerprint of data.
Why “can’t go back” means impossible, not just hard¶
This deserves an argument, because a lot of people assume hashes are merely unbroken so far. They are not.
A hash squeezes inputs of any length into a fingerprint of one fixed length. The word “hello” and the complete works of Shakespeare come out the same size: 32 bytes.
Infinitely many possible inputs, finitely many possible outputs. So many different inputs must collide onto the same fingerprint — that is the pigeonhole principle, not an engineering weakness. Given a fingerprint alone, there is no unique answer to recover.
This is not about computing power. The information is gone.
(And this cleanly separates hashing from encryption. Encryption is a one-to-one map — nothing is lost, so with the key you can go back. Hashing is many-to-one — information is destroyed, so there is no “decrypt” operation to speak of. That distinction becomes important in 1.2.)
Run it¶
We use SHA-256 from hashlib, Bitcoin’s workhorse hash.
import hashlib
def sha256(data: bytes) -> bytes:
'''Take bytes, return a 32-byte fingerprint.'''
return hashlib.sha256(data).digest()
msg = "café42".encode()
h = sha256(msg)
print("fingerprint (hex):", h.hex())
print("length :", len(h), "bytes =", len(h)*8, "bits")
print("hex characters :", len(h.hex()))
fingerprint (hex): d19e9ea45283a659b53117c0614bad5d1774d96fd8587ae3c61ca8893c68b030
length : 32 bytes = 256 bits
hex characters : 64
One detail is easy to skim past: sha256 takes bytes and returns bytes. msg came from
.encode(), and h is bytes too — h.hex() exists only so you can look at it. We keep this
habit for the rest of the book: compute in bytes, display in hex.
The output says 32 bytes = 256 bits. That 256 is where the name comes from.
Two properties you have to see for yourself¶
# Property 1: the length never changes.
short = sha256("hi".encode())
long_ = sha256(("a very long sentence " * 1000).encode())
print("short input ->", len(short), "bytes")
print("21000 chars ->", len(long_), "bytes") # identical
print()
# Property 2: avalanche.
a = sha256("café42".encode()).hex()
b = sha256("café43".encode()).hex() # one digit changed
print("café42 ->", a)
print("café43 ->", b)
diff = sum(1 for x, y in zip(a, b) if x != y)
print("hex characters that differ:", diff, "/ 64")
short input -> 32 bytes
21000 chars -> 32 bytes
café42 -> d19e9ea45283a659b53117c0614bad5d1774d96fd8587ae3c61ca8893c68b030
café43 -> 10fc7d999b3f8c15cc23ab43fdc00b9a2d8656f5509ba25b02b07f93a27c474a
hex characters that differ: 64 / 64
Two things in that output.
First, the two lengths are identical. A 2-character input and a 21,000-character input both produce 32 bytes. That is the pigeonhole argument made visible: you just watched the infinite get crushed into the finite.
Second, diff comes out at 64 out of 64. Every single character changed. One digit
different in the input, and the output is unrecognizable. This is the avalanche effect.
A number worth doing arithmetic on¶
Avalanche is usually described as “flip one bit and half the output changes.” That is true — but it is talking about bits, not the characters on your screen.
We just got 64 out of 64. That is 100%, not half. Why?
Because one hex character is four bits. For the character to look unchanged, all four bits must be unchanged. If each bit flips with probability one half:
P(a hex character survives) = (1/2)⁴ = 1/16 so the expected number that change is 64 × 15/16 = 60
We got 64, which is a slightly lucky draw above the expected 60. And if you count at the bit level, you find 130 out of 256 — 50.8%, clean as advertised.
Both numbers are correct; they measure different things. This is the first time in this book that two true statements look contradictory because the units don’t line up, and it will not be the last (chapter 5’s difficulty, chapter 10’s fee rates). Build the habit now: always ask what the percentage is a percentage of.
Avalanche is not decorative. It is the foundation of everything later. Because of it, you cannot “get warmer” — flip one bit and the result lands somewhere completely unrelated, with no gradient to climb. The reason mining in chapter 5 can only be brute force starts here.
YOUR TURN¶
# YOUR TURN:
# 1) Try two other near-identical strings. Is the hex difference still around 60?
# 2) Count the difference at the bit level and check it lands near half:
# bitdiff = sum(bin(x ^ y).count('1') for x, y in zip(sha256(...), sha256(...)))
# 3) Write a function returning the "first 8 + last 8" short form (how Bitcoin shows a TXID).
def short_id(data: bytes) -> str:
hx = sha256(data).hex()
# hint: hx[:8] and hx[-8:]
return "____" # replace this line
# print(short_id("café42".encode())) # should give d19e9ea4...3c68b030
Question 3 is not filler. Every a1b2c3d4...e5f6a7b8 identifier you will ever see —
including the abbreviated transaction IDs on any block explorer — is produced exactly like that.
One more property, and it sounds boring¶
sha256(b"hello") # today
sha256(b"hello") # next year, on a different machine
b',\xf2M\xba_\xb0\xa3\x0e&\xe8;*\xc5\xb9\xe2\x9e\x1b\x16\x1e\\\x1f\xa7B^s\x043b\x93\x8b\x98$'These are always equal.
sha256 is a pure function: same input, same output, forever. No randomness, no side
effects, no clock, no machine dependence.
That sounds like nothing. It is the entire reason the next section works.
1.2 Commitment: lock now, reveal later¶
A commitment is not a new brick. It is a way of using the hash you already have — your first taste of composition.
The problem it solves¶
You and a friend play a guessing game. You think of a number from 1 to 100; they guess.
The hole is obvious. They guess “50,” and nothing stops you from saying “I was thinking 51, you lose.” There is no referee.
How a commitment closes it: at the start, you publish the hash of your number — write it on the board. After the guess, you publish the number itself, and everyone re-hashes it and compares.
Three properties make it work:
Hiding — from the fingerprint they cannot recover the number (one-wayness)
Binding — after you reveal, they can check (pure function: recomputing must match)
Caught — swap in a different number and the fingerprints don’t match
The part people miss¶
A commitment never needs to be “opened.”
You volunteer the original; it is not extracted from the fingerprint. A hash can never produce its input — and a commitment never asked it to.
So the same property, “you can’t go back,” is fatal for encryption and essential for commitment.
That property flips sides several more times in this book. In chapter 3 it is a weakness (use a hash as an identity and it burns after one use — which is precisely what forced signatures to be invented). In chapter 11 it is the tool that lets one 32-byte root commit to an entire block. In appendix B it is what saves hashes from quantum computers.
Whether a property is a flaw or a feature depends entirely on where you put it.
Three lines¶
def commit(secret: int) -> str:
'''Commitment: publish this fingerprint.'''
return sha256(str(secret).encode()).hex()
def verify(secret: int, commitment: str) -> bool:
'''After the reveal, check it matches.'''
return sha256(str(secret).encode()).hex() == commitment
# --- Opening: you think of 42, publish only the fingerprint ---
c = commit(42)
print("1. published commitment:", c[:16], "...") # nobody sees 42
# --- Reveal: "I was thinking of 42" ---
print("2. honest reveal :", verify(42, c)) # True
# --- Cheat: "actually it was 51" ---
print("3. cheating reveal :", verify(51, c)) # False, caught
1. published commitment: 73475cb40a568e8d ...
2. honest reveal : True
3. cheating reveal : False
Before reading the output, look at the shape of these two functions, because “commitment” sounds far more elaborate than it is:
commit(42)returnsc, which is a 64-character string:73475cb40a568e8d...verify(42, c)recomputes the hash and compares with==
That’s it. A commitment is a hash plus a rule about timing.
The three output lines are the three possible situations: True is honesty, False is
getting caught, and that c on the first line is a value from which nobody can extract anything.
This is composition. Hashing only knows how to make fingerprints. But arrange when the fingerprint is published and when the original is published, and you have a capability that did not exist before.
Play a full round¶
# YOUR TURN: a real two-player game.
# Rule: you pick a number 1-100 and commit; your friend guesses; then you reveal.
import random
# 1) The setter secretly picks a number
answer = random.randint(1, 100) # the computer picks for you
c = commit(answer)
print("committed:", c[:16], "... (the answer is not visible here)")
# 2) Your friend guesses -- change this
guess = 50 # YOUR TURN
print("you guessed:", guess)
# 3) Reveal and check
print("the answer was :", answer)
print("commitment intact? :", verify(answer, c)) # always True
print("did you win? :", guess == answer)
committed: 031b4af5197ec30a ... (the answer is not visible here)
you guessed: 50
the answer was : 51
commitment intact? : True
did you win? : False
The second-to-last line is always True, whether you guessed right or not.
Because it is not checking whether you won. It is checking whether the setter cheated. Those are two separate lines of code and two separate questions, and everyday language smashes them together constantly.
A commitment makes no promise that the outcome is fair. It promises the process was not altered.
The only way to make that line print False is to pass a different number to verify —
i.e. to simulate the setter changing their story.
1.3 The radius of trust¶
So far a commitment looks like a party trick for keeping a friend honest. Its real weight is somewhere else: it lets “you can’t cheat me” reach further and further.
Level one — face to face, enforced by watching. You and your friend are in the same room. Worried they’ll change their answer? Watch them. They write it, seal it in an envelope, you don’t blink.
Enforcement mechanism: physical supervision.
Level two — a city apart, enforced by a hash. They’re far away and you can’t watch. So they send you the hash first (the commitment); after you guess, they send the number, and you recompute it yourself.
Enforcement mechanism: the math watches for you.
Level three — the whole world, enforced by anyone. They broadcast the hash to a crowd of strangers. After the reveal, every one of them can check independently. Nobody has to trust the person at all.
Enforcement mechanism: anyone can verify.
In one sentence:
A commitment moves “you can’t cheat me” from “I have to watch you” (physical), to “math watches for me” (at a distance), to “anyone can watch you” (globally). You stop needing to trust a person and only need to trust arithmetic anyone can redo.
That last step is Bitcoin’s soul. The remaining seventeen chapters are, in a sense, one long exercise in pushing that sentence to a larger scale — from a guessing game to a global monetary system.
But one thing must not get blurred¶
Bitcoin uses that idea. It does not use commitments to implement it.
| Commitment | Spending bitcoin | |
|---|---|---|
| How you prove it | Publish the hidden value (reveal) | Sign with the private key |
| Does the secret appear? | Yes — and it’s spent the moment it does | Never, not once |
| How many times can you do it? | Once | Unlimited |
You need to spend money repeatedly, so the private key can never be published. That rules out the commitment approach entirely — reveal it once and it’s finished.
Bitcoin uses signatures: sign with the private key, let the world verify with the public key, and the private key never appears. Spend ten thousand times, and it still never appears.
Commitments teach us “anyone can verify.” Signatures implement it far more cleverly.
That trick — verifying without revealing — is chapter 3. Before that, chapter 2 has to turn “private key” and “public key” from nouns into numbers you compute yourself.
1.4 The ledger¶
Every chapter ends by counting: of what you just wrote, what was cryptography and what wasn’t.
| What you wrote | Field |
|---|---|
sha256() — a one-way fingerprint | Cryptography |
commit() / verify() — lock first, check later | Cryptography, applied (composition, not a new primitive) |
| “Publish the hash first, the value second” — the ordering | Protocol design |
Almost pure cryptography. Easy tally. Enjoy it — it doesn’t stay this way.
From chapter 4 the right-hand column fills rapidly with systems, consensus, game theory, economics. By chapter 7 you will write an entire chapter of code and find, looking back, not one line of cryptography in it.
(Only two later chapters detour back: chapter 11’s Merkle trees and chapter 13’s real curve. The other thirteen stay on the right.)
Three ideas to carry forward:
Representation — many costumes, one body. Bytes are the thing; hex is for humans.
Pure function — same in, same out. This is why a commitment can be trusted, and ultimately why an entire chain can be independently verified by anyone.
Composition — commitment = hash + a rule about timing. Big capabilities out of small primitives. The rest of this book does nothing but this.
Next¶
Finite fields and elliptic curves.
You will build a clock with exactly 97 positions, find a curve living on it, and start hopping from a fixed point.
How many hops you took is your secret. Where you landed is your public key.
Then you will write a brute-forcer and crack your own private key — and see precisely why the same code, on a real curve, would still be running when the sun burns out.