Merkle Epochs¶
PoC 2 introduces merkle tree epochs -- a compression layer that groups heartbeats into fixed-size epochs, each summarised by a single 32-byte merkle root.
Concept¶
Instead of storing every heartbeat on-chain, heartbeats are grouped into epochs. Each epoch is a merkle tree of heartbeat hashes, yielding a compact root hash. Epochs themselves chain together, forming a verifiable timeline.
Epoch 0 Epoch 1 Epoch 2
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ H0 H1 ... H59 │ │ H60 H61 ... H119 │ │ H120 H121 ... │
│ │ │ │ │ │ │ │ │
│ merkle tree │ │ merkle tree │ │ merkle tree │
│ │ │ │ │ │ │ │ │
│ epoch root │──────│ epoch root │──────│ epoch root │
└──────────────────┘ └──────────────────┘ └──────────────────┘
prevEpochHash prevEpochHash
With 60 heartbeats per epoch (one per minute), each epoch covers 1 hour of uptime.
Merkle Tree Construction¶
Heartbeat hashes are arranged as leaves of a binary merkle tree. The tree is built bottom-up:
Each internal node is the SHA-256 hash of its two children concatenated:
Odd leaf count
If the number of leaves is odd, the last leaf is promoted unchanged to the next level (not duplicated). This avoids inflating the tree with redundant hashes.
Selective Disclosure¶
The key property of merkle trees is selective disclosure: you can prove a specific heartbeat existed within an epoch without revealing any other heartbeats.
Example: Proving Heartbeat #37¶
To prove heartbeat #37 existed in an epoch of 60 heartbeats:
- Provide heartbeat #37 (the leaf).
- Provide the merkle proof path -- the sibling hashes needed to reconstruct the root.
- The verifier hashes heartbeat #37, then combines with each sibling hash up the tree, arriving at the root.
- If the computed root matches the published epoch root, the heartbeat is proven.
Root ← verifier arrives here
/ \
... ...
/ \
... H_sibling ← provided in proof
/
H_sibling ← provided in proof
/
H37 ← start here
The proof path contains O(log n) sibling hashes -- logarithmic in the number of heartbeats per epoch.
Proof Sizes¶
| Heartbeats per Epoch | Tree Depth | Proof Hashes | Proof Size |
|---|---|---|---|
| 60 (1 hour) | 6 | 6 | 192 bytes |
| 1,440 (1 day) | 11 | 11 | 352 bytes |
| 43,200 (30 days) | 16 | 16 | 512 bytes |
Logarithmic scaling
Doubling the number of heartbeats adds only one additional hash (32 bytes) to the proof. This is why merkle trees are the standard approach for compact membership proofs.
Epoch Structure¶
type Epoch struct {
LeaseID string // lease block hash
EpochIndex uint64 // monotonic epoch counter
StartSeq uint64 // first heartbeat sequence in this epoch
EndSeq uint64 // last heartbeat sequence in this epoch
MerkleRoot []byte // root of heartbeat merkle tree
PrevEpochHash []byte // SHA-256 of previous epoch (zero for first)
Timestamp int64 // unix nanos when epoch was sealed
Hash []byte // SHA-256 of this epoch's fields
}
Epoch Chaining¶
Each epoch includes a prevEpochHash field -- the hash of the previous epoch. This creates a chain of epochs analogous to the heartbeat chain itself:
Properties¶
Ordering is immutable. You cannot reorder epochs without breaking the chain of prevEpochHash references.
Insertion is impossible. Inserting a new epoch between two existing ones would require recomputing the prevEpochHash of all subsequent epochs.
Deletion is detectable. Removing an epoch creates a gap: the next epoch's prevEpochHash will not match the preceding epoch's hash.
Verification¶
Verify Epoch Chain Integrity¶
for each epoch Ei where i > 0:
assert Ei.prevEpochHash == SHA-256(Ei-1)
assert Ei.startSeq == Ei-1.endSeq + 1
assert Ei.epochIndex == Ei-1.epochIndex + 1
Verify Heartbeat Membership¶
1. Compute leaf = SHA-256(heartbeat)
2. Walk proof path: for each (sibling, direction) in proof:
if direction == left:
current = SHA-256(sibling || current)
else:
current = SHA-256(current || sibling)
3. Assert current == epoch.merkleRoot
Compression Ratio¶
For a 24-hour lease with 1-minute heartbeats:
| Layer | Data | Size |
|---|---|---|
| Raw heartbeats | 1,440 x ~250 bytes | 360 KB |
| Epoch roots | 24 x 32 bytes | 768 bytes |
| Single claim root | 1 x 32 bytes | 32 bytes |
The epoch layer alone achieves a 480x compression from raw heartbeats. Combined with the claim layer, the total compression reaches 3,681x.
Related Pages¶
- Heartbeat Chain -- the heartbeats that feed into merkle epochs
- Production Design -- how epochs integrate into the three-layer architecture
- Threat Analysis -- attacks against merkle proofs and mitigations