Build a Cryptocurrency
In Modules 1 and 2 you explored the problem and the tools. Now you'll wire those tools together into a working cryptocurrency — wallets, signed transactions, blocks, and an immutable chain — all in about 200 lines of Node.js.
The Big Picture
In Module 1 you understood why we need digital cash. In Module 2 you mastered the four cryptographic tools that make it possible. Now it's time to assemble those tools into a single, working system. By the end of this module you'll have built a mini-Bitcoin from scratch — and you'll understand exactly why each piece is there.
From Tools to System
Module 2 gave you four tools — hash functions, key pairs, digital signatures, and Merkle trees. Each one solved a specific problem in isolation. Now we connect them. Think of it like this: in Module 2 you learned to use a saw, a hammer, nails, and sandpaper. In Module 3, you build the table.
What We'll Build & Why
Each component of our cryptocurrency exists because it solves a specific problem — and each one relies on a tool you already know from Module 2.
The Build Order
We'll build five components in order, each layer depending on the ones below it — like stacking bricks. If you skip ahead, the code won't make sense. If you follow the sequence, each piece will feel inevitable.
Wallet
A key pair (private + public). Your identity on the network — like a passport no government can revoke.
Transaction
A signed instruction: "I send X coins to address Y." Only the owner of the private key can create it.
Block
A container that groups transactions, timestamps them, and seals the batch with a SHA-256 hash.
Chain
An append-only array of blocks linked by hashes. Change one block → every subsequent hash breaks.
Mining
The proof-of-work lottery: find a nonce that produces a hash starting with zeros. This is what makes blocks expensive to forge.
Simplified on Purpose
Our cryptocurrency is a teaching tool, not production software. We use a simplified transaction model (one input / two outputs), skip networking entirely, and run mining at very low difficulty. The goal is to understand the architecture and design decisions, not to replicate every optimization of Bitcoin Core. We'll add decentralisation and peer-to-peer networking in Modules 4 and 5.
Wallets
A wallet is not a bag of coins. It's a key pair — the same kind you generated in Module 2 with elliptic. The private key lets you sign transactions (spend). The public key is your address (receive). There is no username, no email, no bank. Just maths.
You Already Know How to Make One
Remember Module 2, Section 3? You generated an ECDSA key pair on the secp256k1 curve. That key pair is a wallet. The only new thing in this section is wrapping it in a Wallet class with a convenient sign() method. The cryptography hasn't changed — we're just giving it a role.
If a wallet is just a key pair, where are the "coins" actually stored?
Answer: On the Chain, Not in the Wallet
Your coins don't live "in" your wallet. They live on the blockchain as unspent transaction outputs (UTXOs) locked to your public key. Your wallet simply holds the private key that can unlock and spend them. Lose the key → lose the coins forever. There is no "forgot password" button. This is why the Bitcoin community repeats: "Not your keys, not your coins."
From Key Pair to Bitcoin Address
In real Bitcoin, the address you share isn't the raw public key. It goes through a pipeline: Public Key → SHA-256 → RIPEMD-160 → Base58Check. This shortens the address, adds a checksum to catch typos, and adds a version prefix (1… for mainnet). In our simplified code we'll skip this step and use the raw hex public key as the address — but now you know what's really happening.
Node.js: The Wallet Class
const EC = require('elliptic').ec;
const ec = new EC('secp256k1'); // Same curve as Bitcoin
class Wallet {
constructor() {
this.keyPair = ec.genKeyPair(); // random private key → public point
this.publicKey = this.keyPair.getPublic('hex'); // 130 hex chars (uncompressed)
}
// Sign a transaction hash — returns DER-encoded signature
sign(dataHash) {
return this.keyPair.sign(dataHash, 'hex')
.toDER('hex');
}
}
// Demo: create two wallets
const alice = new Wallet();
const bob = new Wallet();
console.log('Alice:', alice.publicKey.slice(0, 40) + '…');
console.log('Bob: ', bob.publicKey.slice(0, 40) + '…');
Wallet = Identity
In traditional banking you prove your identity with a passport and a PIN. In Bitcoin, your identity is your key pair. No government, no bank, no middleman. Anyone on Earth can create a wallet in milliseconds, for free, without asking permission. That single fact — permissionless identity — is one of the most radical properties of cryptocurrency.
Transactions & UTXOs
A transaction is a signed instruction: "I, the owner of this private key, authorise transferring X coins to this public key." That much is simple. What's not simple is how Bitcoin tracks who owns what. It doesn't use account balances like a bank. Instead, it uses a model called UTXO — Unspent Transaction Output. This section explains why.
What Is a UTXO?
Think of UTXOs like physical banknotes in your pocket. If you have a €10 note and want to pay €4, you hand over the entire €10 and get €6 change. You can't tear the note in half. A UTXO works the exact same way: it's an indivisible "coin" from a previous transaction that you must spend entirely, sending the leftover back to yourself as change. Your "balance" is simply the sum of all your unspent notes.
Why didn't Satoshi just use simple account balances? Wouldn't that be easier?
Answer: UTXOs Prevent Double-Spending by Design
With account balances, you need a central authority to check "does Alice really have 10 BTC?" at the exact moment she spends — and that authority must process transactions in strict order. The UTXO model doesn't need that. Each UTXO can only be spent once: the moment it's used as an input, it's destroyed, and new UTXOs are created as outputs. Double-spending a UTXO is like trying to deposit the same physical banknote at two different shops — the second shop will see it's already gone. No central bank required.
How a UTXO Transaction Works
Alice has a UTXO worth 1.0 BTC and wants to send 0.4 BTC to Bob. She can't just "subtract" from a balance. She must consume her entire UTXO and create two new outputs:
After this transaction, Alice's original 1.0 BTC UTXO is destroyed. Two new UTXOs exist: 0.4 BTC belonging to Bob, and 0.6 BTC belonging to Alice. The original "coin" is gone forever — it can never be spent again.
Multiple Inputs, Multiple Outputs
What if Alice wants to send 0.5 BTC but only has two UTXOs of 0.3 each? She combines them as two inputs totalling 0.6 BTC, then creates two outputs: 0.5 to Bob and 0.1 back to herself as change. In real Bitcoin, transactions routinely have dozens of inputs and outputs. The rule is: total inputs ≥ total outputs. The gap is the fee, collected by the miner.
Anatomy of a Transaction
fromnull for a coinbase/mining reward.toamounttimestampsignatureTransaction Rules
- Inputs must reference existing, unspent UTXOs
- Total inputs ≥ total outputs (difference = fee)
- Each input must be signed by the owner's private key
- Once spent, a UTXO can never be used again
Transaction Fees
- Fee = total input − total output
- Higher fee → miner prioritises your TX
- Zero fee → TX may wait forever in the mempool
- As block rewards halve, fees become the main miner incentive
The Coinbase Transaction
Every block contains one special transaction with from = null — the coinbase transaction. This is how new coins enter the system: the miner rewards themselves for doing the proof-of-work. It's the only transaction that creates coins out of thin air; every other transaction merely moves existing coins from one address to another. In our code, you'll see this as new Transaction(null, minerAddress, reward).
Node.js: The Transaction Class
const { createHash } = require('crypto');
const EC = require('elliptic').ec;
const ec = new EC('secp256k1'); // Same curve as the Wallet
class Transaction {
constructor(from, to, amount) {
this.from = from; // sender public key (null = mining reward)
this.to = to; // recipient public key
this.amount = amount;
this.timestamp = Date.now();
this.signature = null; // filled when sign() is called
}
// SHA-256 of the TX data — this is what we sign
calculateHash() {
return createHash('sha256')
.update(this.from + this.to + this.amount + this.timestamp)
.digest('hex');
}
// Only the sender's wallet can sign (enforced by key check)
sign(wallet) {
if (wallet.publicKey !== this.from)
throw new Error('Cannot sign for another wallet!');
this.signature = wallet.sign(this.calculateHash());
}
// Anyone can verify using the sender's public key
isValid() {
if (this.from === null) return true; // coinbase TX — no sender
if (!this.signature) return false; // unsigned = invalid
const key = ec.keyFromPublic(this.from, 'hex');
return key.verify(this.calculateHash(), this.signature);
}
}
Double-Spend Prevention
The entire UTXO system exists to solve the double-spend problem you saw in Module 1. Once a UTXO is consumed as an input, it's destroyed. If Alice tries to spend the same coin twice, the second transaction is rejected because the UTXO no longer exists. No trusted third party needed — just data structures and cryptographic signatures.
Blocks
Transactions float around as individual messages. A block is the container that collects them, timestamps them, and seals the entire batch with a cryptographic hash. Think of a block as a page in a ledger: once written and sealed, it can't be erased without ripping apart the entire book.
Why Batch Transactions Into Blocks?
Why not just add transactions one at a time? Because blocks serve three purposes at once: (1) they amortise the cost of mining across many transactions — mining one block that contains 2,000 TXs is far more efficient than mining 2,000 individual proofs; (2) they create a natural ordering mechanism — every transaction inside block N happened before every transaction inside block N+1; (3) they let us summarise all transactions with a single hash, making validation fast.
Anatomy of a Block
indextimestamptransactionspreviousHashnoncehashWhere's the Merkle Root?
In real Bitcoin, the block header also contains a Merkle root — the single hash that summarises all transactions in the block (remember Module 2, Section 5?). This lets light clients verify that a specific transaction is included without downloading the entire block. We skip it in our simplified code for clarity, but the concept is the same: the Merkle tree lets you prove membership with a tiny proof.
Node.js: The Block Class
const { createHash } = require('crypto');
class Block {
constructor(index, transactions, previousHash) {
this.index = index;
this.timestamp = Date.now();
this.transactions = transactions;
this.previousHash = previousHash; // ← the "link" to the previous block
this.nonce = 0; // ← the miner will change this
this.hash = this.calculateHash();
}
calculateHash() {
return createHash('sha256')
.update(
this.index +
this.timestamp +
JSON.stringify(this.transactions) +
this.previousHash +
this.nonce // nonce changes → hash changes
).digest('hex');
}
// Proof of work: increment nonce until hash starts with zeros
mineBlock(difficulty) {
const target = '0'.repeat(difficulty);
while (this.hash.substring(0, difficulty) !== target) {
this.nonce++;
this.hash = this.calculateHash();
}
console.log('Block mined:', this.hash);
}
}
The Genesis Block
Every blockchain starts with Block 0 — the Genesis Block. It has no previous hash (we use "0") and contains a single coinbase transaction. Bitcoin's genesis block was mined by Satoshi on January 3, 2009, and famously embeds the headline: "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks." — a timestamp proving the block couldn't have been created earlier, and a political statement about why Bitcoin exists.
Real Bitcoin: Block #0 (Genesis)
Hash: 000000000019d6689c085ae165831e93…. Reward: 50 BTC. Transactions: 1 (coinbase only). Nonce: 2,083,236,893. The reward from this block is famously unspendable — due to a quirk in the code, the genesis block's coinbase TX was never added to the UTXO set. Those 50 BTC are locked forever.
The Chain
A blockchain is simply an array of blocks where each block stores the hash of its predecessor. This single design decision is what makes the entire system tamper-evident: change any historical block, and every subsequent hash breaks. Let's see exactly how — and why that matters.
How Blocks Link Together
"0"a1b2c3…a1b2c3…d4e5f6…d4e5f6…7g8h9i…The Tamper Cascade
Imagine an attacker changes a transaction in Block 1 (say, changing "10 BTC" to "1000 BTC"). Block 1's hash changes because the data changed. But Block 2 stores Block 1's old hash — so Block 2 is now invalid. The attacker must re-mine Block 2 to fix it. But then Block 3 becomes invalid, and Block 4, and so on. The attacker must re-mine every subsequent block, faster than the entire honest network is mining new ones. With Bitcoin's hashrate (hundreds of EH/s as of 2024), rewriting even 6 blocks is practically impossible. This is why we say: the deeper a block is in the chain, the more immutable it becomes.
Node.js: The Blockchain Class
class Blockchain {
constructor() {
this.chain = [this.createGenesis()];
this.difficulty = 2; // hash must start with "00"
this.pending = []; // mempool — TXs waiting to be mined
this.miningReward = 50; // coinbase reward per block
}
createGenesis() {
return new Block(0, [], '0'); // index 0, no TXs, no previous hash
}
getLatest() {
return this.chain[this.chain.length - 1];
}
// Validate & add to mempool — signature check here!
addTransaction(tx) {
if (!tx.from || !tx.to)
throw new Error('TX must have from and to');
if (!tx.isValid())
throw new Error('Cannot add invalid TX');
this.pending.push(tx);
}
// Build a block from pending TXs, mine it, append to chain
minePending(minerAddress) {
// 1. Create coinbase reward TX
const reward = new Transaction(null, minerAddress, this.miningReward);
this.pending.push(reward);
// 2. Build block with all pending TXs
const block = new Block(
this.chain.length, // next index
this.pending, // transactions
this.getLatest().hash // previous block's hash
);
// 3. Mine: find the nonce (proof of work)
block.mineBlock(this.difficulty);
// 4. Append to chain & clear mempool
this.chain.push(block);
this.pending = [];
}
// Walk the entire chain to compute an address's balance
getBalance(address) {
let balance = 0;
for (const block of this.chain)
for (const tx of block.transactions) {
if (tx.from === address) balance -= tx.amount;
if (tx.to === address) balance += tx.amount;
}
return balance;
}
// Verify every block: recalc hash + check previousHash link
isValid() {
for (let i = 1; i < this.chain.length; i++) {
const curr = this.chain[i];
const prev = this.chain[i - 1];
if (curr.hash !== curr.calculateHash()) return false;
if (curr.previousHash !== prev.hash) return false;
}
return true;
}
}
Chain Validation — Two Simple Checks
The isValid() method walks the entire chain and checks two things for every block: (1) Does the stored hash match a fresh recalculation? If not, the block's data was modified. (2) Does previousHash actually equal the preceding block's hash? If not, someone inserted or reordered a block. Both checks are O(n) — just one pass through the array. This is the fundamental guarantee of blockchain integrity, and it's remarkably simple.
Mining & Proof of Work
Mining is the process that turns pending transactions into a sealed, immutable block. The miner's job: find a nonce (a number) such that the block's SHA-256 hash starts with a certain number of zeros. Sound familiar? This is exactly the HashCash idea from Module 1 — the same mechanism Adam Back invented in 1997 to fight email spam.
From Email Spam to Digital Gold
In Module 1 you learned that HashCash forced email senders to "pay" with CPU time: find a nonce so the hash starts with zeros. Legitimate senders compute one stamp per email; spammers can't afford millions. Satoshi adapted the same idea: instead of stamping emails, miners stamp blocks. The "payment" isn't in cash — it's in electricity spent computing hashes. This is what gives proof of work its security: forging a block is computationally expensive.
Step by Step: How Mining Works
The miner takes all block fields — index, timestamp, transactions, previousHash — and appends a nonce starting at 0. It hashes everything with SHA-256. If the hash doesn't start with enough zeros, it increments the nonce and tries again. With difficulty 2, it needs a hash starting with 00 (1 in 256 chance). With difficulty 4, it needs 0000 (1 in 65,536). Each additional zero multiplies the expected work by 16. Bitcoin's current difficulty requires roughly 19 leading zeros.
Mining in Action
2
hash must start with "00"
a9f3c1…
✗
7b2e08…
✗
f41d92…
✗
00a8f7…
✓
Difficulty Adjustment
Bitcoin targets one block every 10 minutes. But hashrate fluctuates as miners join or leave the network. So every 2,016 blocks (~2 weeks), the protocol automatically adjusts the difficulty: if blocks came too fast, difficulty increases; too slow, it decreases. This self-regulating mechanism ensures a predictable coin issuance schedule regardless of how much mining hardware exists. In our code we use a fixed difficulty for simplicity.
Why Mine?
- Block reward: the miner earns new coins (currently 3.125 BTC, as of 2024 halving)
- Transaction fees: the miner collects fees from all TXs in the block
- These are the only two ways new coins enter the system
The Halving
- Every 210,000 blocks (~4 years), the reward halves
- 50 → 25 → 12.5 → 6.25 → 3.125 BTC (2024)
- Maximum supply: 21 million BTC — built-in scarcity, enforced by code
What's Still Missing?
Our miner runs on a single machine — there's no competition, no network, no one to disagree with. Real Bitcoin has thousands of miners racing simultaneously, a peer-to-peer gossip protocol, and a rule that says "the longest valid chain wins." How do they coordinate without a leader? That's the topic of Module 4: Decentralisation & Proof of Work at Scale.
Putting It All Together
You've now seen every piece. Let's trace the full lifecycle of a payment — from the moment Alice decides to pay Bob, to the moment the transaction is permanently etched in the chain. This is the "aha" moment where all five components click together.
Lifecycle: Alice Sends 10 Coins to Bob
Her wallet loads her private key from disk. The corresponding public key is her address — the one she gave Bob earlier so he could verify payments.
new Transaction(alice.publicKey, bob.publicKey, 10). The TX is not yet signed — it's just a data structure saying "10 coins from Alice to Bob."
tx.sign(aliceWallet). The sign() method computes the TX hash (SHA-256) and signs it with her private key (ECDSA). This proves she authorised the payment — without revealing her private key. (Module 2: Digital Signatures)
chain.addTransaction(tx). The blockchain validates the signature using Alice's public key (anyone can do this — that's the beauty of asymmetric crypto). If valid, the TX is added to the pending array — the "waiting room" for unconfirmed transactions.
chain.minePending(miner.publicKey). The miner grabs all pending TXs, adds a coinbase reward TX to themselves, and builds a new Block object with previousHash pointing to the latest block.
block.mineBlock(difficulty). The miner increments the nonce over and over, hashing each time, until the hash starts with enough zeros. This is the expensive part — the "proof" that real computational work was done. (Module 1: HashCash)
The sealed block is pushed onto the chain array. Its hash is now the previousHash for the next block. The chain grows by one link. The mempool is cleared.
Bob's balance increases by 10 coins. He can verify this by calling chain.getBalance(bob.publicKey). If he wants to spend those coins, he'll create a new Transaction and the cycle begins again.
Module 2 Tools in Action
Notice how every Module 2 tool appeared: Key pairs (wallet identity), SHA-256 (TX hash, block hash, mining puzzle), Digital signatures (TX authorisation & verification), Merkle trees (in real Bitcoin, summarising TXs in the block header). These four tools, combined in exactly this way, are what makes trustless digital cash possible.
What Can Go Wrong?
Step 3: If Bob tries to forge Alice's signature, isValid() returns false — the TX is rejected.
Step 4: If Alice tries to double-spend, the UTXO is already consumed — the second TX fails.
Step 6: If a miner submits a block with an invalid proof of work, other nodes reject it.
Step 7: If someone tampers with a historical block, chain.isValid() catches the broken hash chain.
Every failure mode is caught by a simple, verifiable check. No trust required.
Exercise: Build Your Own Chain
Time to assemble everything yourself. These exercises walk you through combining all the pieces into a working mini-cryptocurrency. Work through them in order — each one builds on the last.
Setup
Create a new project folder with npm init -y and install elliptic: npm install elliptic. You'll need three files: wallet.js (Wallet class), blockchain.js (Transaction, Block, Blockchain classes), and main.js (your demo script). Or simply use the Playground link below.
Exercises
- Create wallets: Instantiate three wallets (Alice, Bob, Charlie). Print each public key. How many hex characters long is each address? Why that specific length?
- Send coins: Mine a first block (rewarding the miner). Then create a transaction from the miner to Alice for 25 coins, sign it, and mine a second block. Print everyone's balances. Do the numbers add up? Where did the "extra" coins come from?
- Chain validation: After mining two blocks, call
chain.isValid(). Now manually tamper with a transaction amount:chain.chain[1].transactions[0].amount = 9999. Check validity again. Why does it fail? Which of the two checks inisValid()caught the tampering? - Difficulty experiment: Change difficulty from 2 to 4. Mine a block using
console.time('mine') / console.timeEnd('mine'). How many nonces were tried? How does the time compare to difficulty 2? Can you predict difficulty 6? - Forgery attempt: Create a transaction from Alice to Charlie but try to sign it with Bob's wallet. What error do you get? Now try submitting an unsigned transaction via
addTransaction(). What happens?
Self-Check Questions
- What's the difference between a wallet and a Bitcoin address? (Hint: our code uses the raw public key as the address — real Bitcoin adds extra steps.)
- Why can't you "partially spend" a UTXO? What must you do instead?
- In our code,
getBalance()walks the entire chain. Why is this slow? How does real Bitcoin solve this? (Hint: think about what the "UTXO set" is.) - If a miner tampers with a transaction in block 5 of a 10-block chain, how many blocks must they re-mine?
- Name the four Module 2 tools and explain where each one appears in our cryptocurrency code.
Module Summary
You've built a working cryptocurrency from scratch. Wallets create identity through key pairs. Transactions transfer value through signed messages. Blocks batch and seal transactions with hashes. The chain links blocks into an immutable history. Mining makes forgery expensive through proof of work. Every major blockchain in the world — Bitcoin, Ethereum, and beyond — uses these same building blocks. In the next module, we'll take this single-machine system and distribute it across a network of untrusting nodes.