Building the Chain — From Blocks to an Append-Only Ledger
We have cryptographic primitives. We have blocks. What we don't have is a ledger — a structure where history can be written but never rewritten. Today we build one, and discover why append-only isn't a limitation but a guarantee.
I'll admit something that took me longer than it should have to understand: I kept thinking of a blockchain as a linked list with extra steps.
Nodes pointing to previous nodes. A chain of references. The data structure we all learned in university, dressed up in cryptographic clothing. I wasn't wrong about the shape — I was wrong about the semantics. A linked list says "this node comes after that node." A blockchain says "this block commits to the entire history that preceded it." The pointer in a linked list is a memory address. The pointer in a blockchain is a cryptographic proof.
That distinction changes everything about what the structure can guarantee. And it maps onto a principle that accountants understood centuries before computer science existed.
The oldest immutable ledger
Double-entry bookkeeping was formalized by Luca Pacioli in 1494 — a Franciscan friar and mathematician who published what amounted to the accounting profession's founding document. The core principle: every transaction is recorded twice, as a debit in one account and a credit in another. The two entries must balance. If they don't, someone made an error — or committed fraud.
But the deeper principle is the one that matters for us: entries are never erased. If you make a mistake in a double-entry ledger, you don't go back and change the original entry. You add a new correcting entry. The original mistake stays in the record, permanently. The ledger is append-only by design, because the moment you allow erasure, you lose the ability to audit.
Five hundred years before anyone wrote a line of code, accountants discovered the same principle we're implementing today: if you want a trustworthy record, you don't edit history — you extend it.
Our blockchain is a digital version of Pacioli's ledger. Each block is a page in the book. Each page contains transactions. And each page is cryptographically bound to every page that came before it — not through ink and paper, but through hash chains.
From data to transaction
The SHA-256 deep dive gave us the cryptographic primitives: hashing for tamper detection, signatures for identity, Merkle trees for efficient verification. Now we need the thing those primitives protect — the actual data.
A transaction in our chain is a signed transfer of value between two parties:
public class Transaction
{
public string From { get; init; } = string.Empty;
public string To { get; init; } = string.Empty;
public decimal Amount { get; init; }
public long Timestamp { get; init; }
public byte[] Signature { get; init; } = Array.Empty<byte>();
public byte[] ToBytes()
{
var data = $"{From}{To}{Amount}{Timestamp}";
return Encoding.UTF8.GetBytes(data);
}
public bool Verify(byte[] publicKey)
{
using var ecdsa = ECDsa.Create();
ecdsa.ImportSubjectPublicKeyInfo(publicKey, out _);
return ecdsa.VerifyData(ToBytes(), Signature,
HashAlgorithmName.SHA256);
}
}The Verify method is the critical piece. Anyone can claim "Alice sent 50 to Bob." But only Alice's private key can produce a signature that passes VerifyData against Alice's public key. The transaction carries its own proof of authenticity — no trusted third party needed.
Notice what's not here: there's no account balance check. Our chain doesn't know whether Alice has 50 to send. That validation happens at a higher level — when we decide whether to include a transaction in a block. The transaction itself is just a signed claim.
The block as a sealed page
A block is a container that groups transactions together and seals them with a cryptographic summary:
public class Block
{
public int Index { get; init; }
public long Timestamp { get; init; }
public byte[] PreviousHash { get; init; } = Array.Empty<byte>();
public List<Transaction> Transactions { get; init; } = new();
public int Nonce { get; set; }
public byte[] MerkleRoot => ComputeMerkleRoot();
public byte[] Hash => ComputeHash();
private byte[] ComputeMerkleRoot()
{
if (Transactions.Count == 0)
return SHA256.HashData(Array.Empty<byte>());
var leaves = Transactions
.Select(tx => tx.ToBytes())
.ToList();
var currentLevel = leaves
.Select(l => SHA256.HashData(l))
.ToList();
while (currentLevel.Count > 1)
{
var nextLevel = new List<byte[]>();
for (int i = 0; i < currentLevel.Count; i += 2)
{
var left = currentLevel[i];
var right = i + 1 < currentLevel.Count
? currentLevel[i + 1]
: currentLevel[i];
var combined = new byte[64];
left.CopyTo(combined, 0);
right.CopyTo(combined, 32);
nextLevel.Add(SHA256.HashData(combined));
}
currentLevel = nextLevel;
}
return currentLevel[0];
}
private byte[] ComputeHash()
{
var header = new byte[4 + 8 + 32 + 32 + 4];
BitConverter.GetBytes(Index).CopyTo(header, 0);
BitConverter.GetBytes(Timestamp).CopyTo(header, 4);
PreviousHash.CopyTo(header, 12);
MerkleRoot.CopyTo(header, 44);
BitConverter.GetBytes(Nonce).CopyTo(header, 76);
return SHA256.HashData(header);
}
}Two things are worth noting here. First, the hash is computed from binary data, not string concatenation. The Byzantine Generals investigation used string-based hashing for clarity — that was fine for a first pass. But string concatenation has a subtle problem: Index=1, Timestamp=23 and Index=12, Timestamp=3 could produce the same string "123". Binary serialization with fixed-width fields eliminates this class of collision.
Second, the MerkleRoot is computed on demand. In a production system, you'd compute it once and cache it. But for our exploration, computing it fresh makes the relationship explicit: the Merkle root is a function of the transactions, and changing any transaction changes the root, which changes the block's hash.
The chain: linking time to proof
Now the structure that gives the blockchain its name:
public class Blockchain
{
private readonly List<Block> _chain = new();
private readonly List<Transaction> _pendingTransactions = new();
public IReadOnlyList<Block> Chain => _chain;
public Blockchain()
{
_chain.Add(CreateGenesisBlock());
}
private static Block CreateGenesisBlock()
{
return new Block
{
Index = 0,
Timestamp = new DateTimeOffset(2009, 1, 3, 18, 15, 5,
TimeSpan.Zero).ToUnixTimeSeconds(),
PreviousHash = new byte[32],
Transactions = new List<Transaction>(),
Nonce = 0
};
}
public void AddTransaction(Transaction transaction)
{
// In a real chain: verify signature, check balance, validate format
_pendingTransactions.Add(transaction);
}
public Block MineBlock()
{
var block = new Block
{
Index = _chain.Count,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
PreviousHash = _chain[^1].Hash,
Transactions = new List<Transaction>(_pendingTransactions),
Nonce = 0
};
_chain.Add(block);
_pendingTransactions.Clear();
return block;
}
}The genesis block is special — it has no predecessor, so its PreviousHash is all zeros. I used Bitcoin's actual genesis timestamp (January 3, 2009, 18:15:05 UTC) as a nod to history, but any fixed value works. The genesis block is a convention, not a computation.
MineBlock is deliberately simple right now. It creates a block, links it to the previous block's hash, and adds it to the chain. There's no proof-of-work requirement yet — any node can add a block instantly. That's a problem proof of work solves.
Why the chain can't be rewritten
The append-only guarantee isn't enforced by access control or permissions. It's enforced by mathematics. Here's how:
public bool ValidateChain()
{
for (int i = 1; i < _chain.Count; i++)
{
var current = _chain[i];
var previous = _chain[i - 1];
// Does this block's PreviousHash match?
if (!current.PreviousHash.SequenceEqual(previous.Hash))
return false;
// Is the block's own hash consistent?
if (!current.Hash.SequenceEqual(current.ComputeHash()))
return false;
}
return true;
}Suppose an attacker wants to change a transaction in block 5 of a 100-block chain. Changing the transaction changes block 5's Merkle root. The changed Merkle root changes block 5's hash. But block 6's PreviousHash field contains the original hash of block 5. Now block 6 is invalid. The attacker must recompute block 6's hash with the new PreviousHash. But that changes block 6's hash, which invalidates block 7. The attacker must recompute every subsequent block — 95 blocks in this example.
Without proof of work, that recomputation is trivial — just a loop of SHA-256 calls. Which is exactly why proof of work exists: it makes each block recomputation deliberately expensive. But the chain structure itself is what detects the tampering. Proof of work is what makes fixing the detection impractical.
Testing the guarantee
Let's prove that tampering breaks the chain:
public static void DemonstrateTamperDetection()
{
var chain = new Blockchain();
// Add some transactions and mine blocks
chain.AddTransaction(new Transaction
{
From = "Alice", To = "Bob", Amount = 50,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Signature = Array.Empty<byte>() // simplified for demo
});
chain.MineBlock();
chain.AddTransaction(new Transaction
{
From = "Bob", To = "Charlie", Amount = 30,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Signature = Array.Empty<byte>()
});
chain.MineBlock();
Console.WriteLine($"Valid before tampering: {chain.ValidateChain()}");
// Output: True
// Tamper with block 1's transaction
chain.Chain[1].Transactions[0] = new Transaction
{
From = "Alice", To = "Mallory", Amount = 50,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Signature = Array.Empty<byte>()
};
Console.WriteLine($"Valid after tampering: {chain.ValidateChain()}");
// Output: False — block 2's PreviousHash no longer matches
}The chain breaks immediately. Not because we added a security check — because the mathematics are inconsistent. Block 2 was computed with block 1's original hash. Changing block 1's transactions changes its Merkle root, which changes its hash. Block 2's PreviousHash now points to a hash that doesn't exist. The chain is its own audit trail.
What an append-only ledger costs
This design has costs.
Storage grows forever. Every transaction in the history of the chain is stored by every full node. Bitcoin's chain is over 550 GB as of 2024. You can't delete old blocks — that's the whole point. Pruning strategies exist (storing only UTXO sets instead of full transaction history), but full nodes must keep everything.
Mistakes are permanent. If you send funds to the wrong address, there's no "undo" button. The double-entry bookkeeping principle cuts both ways: immutability protects against fraud but also against honest errors. The correcting entry (sending the funds back) requires the recipient's cooperation.
Throughput is limited by block size and block time. Bitcoin processes roughly 7 transactions per second. Visa processes roughly 65,000. The gap isn't a bug — it's a direct consequence of the design: every node must validate every transaction. Scaling strategies (larger blocks, faster block times, layer-2 networks) each introduce their own trade-offs.
These aren't criticisms. They're costs — and knowing the costs is how you decide whether the guarantee is worth paying for. For a global, censorship-resistant store of value, the costs may be justified. For tracking inventory in a warehouse, they almost certainly aren't. The data structure doesn't change. The question of whether it's appropriate changes with every use case.
The missing piece
We have a chain that detects tampering. We have transactions that prove their origin. We have Merkle trees that allow efficient verification. What we don't have is a reason why any particular node should get to add the next block.
Right now, MineBlock is a method call. Anyone can call it. If two nodes call it simultaneously with different transactions, we have two valid but conflicting chains. Which one wins?
The answer — proof of work — turns block creation from a function call into a computational lottery. The node that finds the winning ticket first earns the right to extend the chain. That search is deliberately, wastefully, necessarily expensive. And the mathematics of why it works connect directly to the birthday bound we measured in the SHA-256 investigation.
The chain we built here is a ledger. Next, we make it a consensus mechanism.



