Skip to content

Day 27 · Proof of History

Day 26 named the first bottleneck Solana had to beat: agreeing on order is expensive. In a distributed system there is no global clock you can trust — machines disagree about the time, and a malicious node can lie about when it saw a transaction. The classic fix is for validators to talk: exchange messages until they agree on an ordering. At thousands of transactions per second, that conversation is the bottleneck. Today you build Solana’s answer — a clock that needs no conversation because it is verifiable by anyone, from the data alone.

The trick: a chain you can’t fake without doing the work

Section titled “The trick: a chain you can’t fake without doing the work”

Take SHA-256 and feed its output back into itself, over and over:

h0 = sha256(seed)
h1 = sha256(h0)
h2 = sha256(h1)
hN = sha256(h(N-1))

Now ask: how do you know hN? Because SHA-256 is pre-image resistant, there is no shortcut — the only way to produce hN is to actually compute all N hashes, one after another. You cannot skip ahead, and critically you cannot parallelize the production: each hash needs the previous one’s output as its input, so a thousand cores are no faster than one. That makes the chain a count of real, sequential work — and since each hash takes a roughly fixed time on a given machine, a count of hashes is a measure of elapsed time. The chain is a clock that ticks once per hash, and the tick is impossible to forge cheaply.

To stamp an event into that timeline, you mix it in: at some point you do one hash over the current state concatenated with the event, instead of over the state alone:

normal tick: h_next = sha256(h)
record event: h_next = sha256(h || sha256(event))

Every hash after that point depends on the event, so the event provably happened after everything before it and before everything after it. Order is established by construction — no timestamps, no agreement protocol. That’s the whole idea.

genesis ─►(tick)─►(tick)─►(record A)─►(tick)─►(record B)─►(tick)─►
▲ ▲
A is sealed here B is sealed here, after A

Start with the primitives. Hash is just a 32-byte array — fixed size, lives on the stack, Copy:

use sha2::{Digest, Sha256};
pub type Hash = [u8; 32];
pub fn sha256(input: &[u8]) -> Hash {
let mut h = Sha256::new();
h.update(input);
h.finalize().into() // GenericArray<u8, 32> -> [u8; 32]
}
pub fn hash_once(h: &Hash) -> Hash { sha256(h) } // a bare tick
pub fn hash_with(h: &Hash, mix: &Hash) -> Hash { // a tick that folds an event in
let mut hasher = Sha256::new();
hasher.update(h);
hasher.update(mix);
hasher.finalize().into()
}
pub fn mixin_of(data: &[u8]) -> Hash { sha256(data) } // reduce any event to 32 bytes

The generator owns the rolling state and a running count of work done. Each Entry it emits is a checkpoint: how many hashes since the last entry, the resulting hash, and — if this entry recorded an event — the value that was mixed in.

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Entry {
pub num_hashes: u64, // hashes since the previous entry (>= 1)
pub hash: Hash, // chain state right after this entry
pub mixin: Option<Hash>, // the event folded in at the last hash, if any
}
pub struct Poh { state: Hash, count: u64 }
impl Poh {
pub fn new(seed: &[u8]) -> Self { Poh { state: sha256(seed), count: 0 } }
pub fn tick(&mut self, n: u64) -> Entry { // "time passed, nothing happened"
for _ in 0..n { self.state = hash_once(&self.state); self.count += 1; }
Entry { num_hashes: n, hash: self.state, mixin: None }
}
pub fn record(&mut self, n: u64, data: &[u8]) -> Entry { // seal an event into the stream
for _ in 0..n - 1 { self.state = hash_once(&self.state); self.count += 1; }
let mix = mixin_of(data);
self.state = hash_with(&self.state, &mix);
self.count += 1;
Entry { num_hashes: n, hash: self.state, mixin: Some(mix) }
}
}

Notice the shape of record: it does n - 1 bare hashes, then one hash that mixes the event in — so num_hashes always counts total hashes including the mixing one. That bookkeeping is exactly what the verifier needs.

Verify it: the same work, but anyone can check it

Section titled “Verify it: the same work, but anyone can check it”

Verification replays the chain from the genesis hash and confirms every recorded hash matches. For a tick, redo num_hashes bare hashes; for a record, redo num_hashes - 1 bare hashes then one with the mixin:

pub fn verify(start: Hash, entries: &[Entry]) -> bool {
let mut cur = start;
for e in entries {
if e.num_hashes == 0 { return false; }
match e.mixin {
None => for _ in 0..e.num_hashes { cur = hash_once(&cur); },
Some(mix) => {
for _ in 0..e.num_hashes - 1 { cur = hash_once(&cur); }
cur = hash_with(&cur, &mix);
}
}
if cur != e.hash { return false; } // first mismatch = forged or corrupt
}
true
}

The crate’s unit tests pin down the three properties that make this a clock you can trust:

// 1. A clean chain verifies, and the verifier does exactly the producer's work.
assert!(verify(genesis(seed), &entries));
assert_eq!(total_hashes(&entries), poh.count());
// 2. Flip a single byte of any recorded hash and verification fails — tamper-evident.
forged[1].hash[0] ^= 0x01;
assert!(!verify(genesis(seed), &forged));
// 3. Swap two recorded events and the chain no longer verifies — order is structural.
let swapped = vec![b, a];
assert!(!verify(genesis(seed), &swapped));

Run cargo test poh and watch all three pass. The third is the one to sit with: you cannot reorder two events even if you keep both of them, because each event’s position is baked into everything hashed after it. That is what “ordered by construction” buys you.

Under the hood — why production is serial but verification is parallel

Section titled “Under the hood — why production is serial but verification is parallel”

This is the asymmetry that makes PoH pay off. Producing the chain is unavoidably sequential: hash k+1 needs hash k, so the leader’s clock advances at exactly one core’s hashing speed, no faster. Verifying it, though, is embarrassingly parallel — because every Entry records its own ending hash, a verifier already knows the start hash of each segment (it’s the previous entry’s hash). So it can hand segment 0→1 to core 0, segment 1→2 to core 1, and so on, checking them all at once:

produce (1 core): ─h─h─h─h─h─h─h─h─h─h─► (must be sequential)
verify (4 cores): ├──seg──┤├──seg──┤├──seg──┤├──seg──┤ (independent, parallel)

With K cores a verifier checks roughly faster than it could produce — a forger gets no such help, because to fake a future state they’d have to actually run the sequential chain. (Our verify is linear for clarity; a real validator splits it across cores, and you could too with the same rayon tools you’ll meet on Day 29.) This is the same “fast to check, slow to produce” shape as Bitcoin’s proof-of-work, turned to a different purpose: not picking a leader, but timestamping.

Here’s the payoff for the project’s throughput question. Once there’s a verifiable clock, the leader can simply stream transactions into the PoH chain as it receives them — each record stamps a transaction into a fixed position — and broadcast the entries. Every other validator verifies the chain locally and in parallel, and now everyone agrees on the order without a single round of “what time did you see this?” messaging. Ordering, which was a chatty consensus sub-problem on other chains, becomes a property you read off the data. That decoupling — order first and cheaply via PoH, then vote on validity via PoS/Tower BFT — is a big part of how Solana keeps the pipeline fed at high transaction rates.

What did building the clock force you to understand? The difference between sequential and parallelizable work — the entire value of PoH lives in that distinction, and you’ll meet its mirror image on Day 29 when execution goes the other way (independent, so parallel). And what is Rust protecting you from? Less drama than usual today, but a real point: the rolling state: Hash is a [u8; 32] the generator owns, advanced by value each step, so there’s exactly one source of truth for “where the clock is now” — no aliasing, no two writers, nothing to desync. Determinism is the property the whole chain depends on, and Rust’s ownership of that single rolling value is what guarantees it.

→ Next: Day 28 · The Account Model · Prev: Day 26 · The Throughput Problem · Back to the Project 7 overview

  1. Why can’t the production of a PoH chain be parallelized, and why does that make a hash count a measure of elapsed time?
  2. What does “mixing an event in” actually compute, and what guarantee does it give about that event’s position in the timeline?
  3. Production is serial but verification is parallel. Explain precisely what makes verification parallelizable, given the contents of an Entry.
  4. The page says iterated SHA-256 is “VDF-like but not a strict VDF.” What’s the difference, and what is PoH’s security actually resting on?
  5. How does having a cheap, verifiable clock raise a chain’s transaction throughput — i.e. what expensive step does it remove from agreeing on order?
Show answers
  1. Each hash takes the previous hash as its input (h_next = sha256(h)), so the steps form a strict dependency chain — extra cores can’t help, the chain advances at one core’s speed. Because each hash costs a roughly fixed time on a given machine, the number of hashes done is proportional to real elapsed time.
  2. It computes sha256(state || sha256(event)) — one hash that folds the event into the rolling state. From then on every hash depends on the event, so the event is provably after everything earlier in the chain and before everything later; its order is fixed by construction and can’t be changed without redoing all subsequent work.
  3. Each Entry records its own ending hash, which is the starting hash of the next segment. So a verifier already knows the start and claimed end of every segment between entries and can recompute each segment independently on a different core — the segments don’t depend on each other’s recomputation, only on the recorded boundary hashes.
  4. A strict VDF has verification that’s asymptotically cheaper than evaluation (e.g. logarithmic in the work). PoH verification does the same number of hashes as production — it’s only parallelizable, not fundamentally less work. Its security rests on SHA-256 having no shortcut faster than actually recomputing the sequential chain.
  5. It removes the inter-validator messaging needed to agree on the order/timing of transactions. Instead of a chatty protocol exchanging “when did you see this?”, the leader streams transactions into the PoH chain and everyone verifies the order locally and in parallel — ordering becomes a property read from the data, freeing the pipeline to push far more transactions per second.