Proof of History
Proof of History (PoH) is a verifiable delay function based on iterated SHA-256. ShardoChain uses it as a shared clock — a sequence of hashes every validator can compute and verify independently — so leader rotation and slot timing don't require consensus round-trips.
What PoH is, and is not
PoH is not a consensus protocol. The fork-choice rule and safety guarantees come from HotStuff-2. PoH provides:
- A deterministic slot-based timer all nodes agree on.
- A cryptographic timestamp: a hash
h_Nproves at least N×SHA-256 worth of CPU time elapsed sinceh_0. - An anchor for events: TX hashes and votes can be mixed in, proving when they occurred relative to the chain.
The verifiable delay function
The unit is PohHash = 32-byte SHA-256. Two operations:
impl PohHash { /// Advance the chain by one step: SHA-256(self). pub fn next(&self) -> Self; /// Mix external data: SHA-256(self || data). pub fn mix(&self, data: &[u8]) -> Self; }
next() is not parallelisable —
to reach the N-th hash you must compute the previous N-1 in sequence. A chain of K iterations is a mathematical proof of K / hashes_per_second wall-clock time, verifiable by any observer.
Configuration
| Parameter | Testnet | Mainnet | Meaning |
|---|---|---|---|
slot_duration_ms | 700 | 800 | Target wall-clock slot duration |
ticks_per_slot | 8 | 8 | Tick subdivisions of one slot |
hashes_per_tick | 12 500 | 12 500 | SHA-256 iterations per tick (calibrated for x86-64 SHA-NI) |
slots_per_epoch | 432 000 | 432 000 | ~80 h per epoch |
With slot_duration_ms = 700, theoretical
max throughput is 1.43 blocks/second. Multiple v5.1.x
soaks have sustained this rate for hours.
PoH and block production
The leader for (height, view) is:
fn leader_for(set: &[Validator], height: u64, view: u64) -> Address { let idx = (height + view) % set.len(); set[idx].address }
Each validator's local PoH recorder advances by tick()
once per tick_duration_ms ≈ 87 ms. After 8 ticks, the slot increments. At slot start, the validator checks if it's the leader — if so, drains the mempool, executes, signs the block.
PoH vote
struct PohVote { slot: u64, block_hash: Hash256, voter: Address, signature: [u8; 64], // over borsh(slot, block_hash, voter, view) view: u64, // v5.1.0+: prevents cross-view replay }
The signature covers view so a vote at
view 0 cannot be replayed at view 1.
Persistence
The PohVoteRegistry snapshots itself to
data_dir/vote_registry.bin every ~5 s
with retention 512 slots (~6 min). On restart, the snapshot is loaded
before the first slot fires. Without this (pre-v5.0.47) a restarted
validator would start with an empty registry and the cluster would
stall waiting for parent-quorum reconstruction.
v5.1.8 added explicit fsync on the temp
file plus parent-directory fsync after
rename, making the snapshot durable across power loss.
Slot tiebreak in fork-choice
Under sustained burst, two competing blocks at the same height with
equivalent QC weight can emerge (BFT safety violation). PoH solves
this deterministically: the block with the lower slot wins
— every validator sees the same slot for the same hash because slots
are time-derived. Implemented in fork_detector.rs::fork_choice_rule
(v5.0.32, commit 2fdb3f7).
Light-client verification
shardo-poh::verifier::verify_chain
replays the chain from a known checkpoint to a target hash and counts
SHA-256 iterations. The delta is a mathematical proof of elapsed time.
Cost is O(N) hashes — verifying 1 hour of testnet (~5 142 slots × 8 ticks
× 12 500 hash ≈ 514 M hashes) takes ~30 s on a SHA-NI-capable CPU.
Known limitations
- Clock skew. A validator with NTP drift >50 ms can silently miss its slot. The
poh_clock_skew_slotsPrometheus gauge (v5.1.8) surfaces this; alert atabs() > 10sustained 30 s. - Hardware-dependent timing. 12 500 hashes/tick is calibrated for x86-64 with SHA-NI. ARM Cortex without SHA-256 hardware would need recalibration.
- Per-shard PoH. Each shard runs an independent PoH. Cross-shard ordering is a planned post-mainnet feature.