Merkle Epochs
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 prevEpochHashWith 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:
Root
/ \
H01 H23
/ \ / \
H0 H1 H2 H3Each internal node is the SHA-256 hash of its two children concatenated:
parent = SHA-256(left || right)[!NOTE] 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 hereThe 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
[!SUCCESS] 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:
E0 ← E1 ← E2 ← E3 ← ... ← EnProperties
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 + 1Verify 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.merkleRootCompression 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