Skip to content

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:

         Root
        /    \
      H01     H23
     /   \   /   \
    H0   H1 H2   H3

Each internal node is the SHA-256 hash of its two children concatenated:

parent = SHA-256(left || right)

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:

  1. Provide heartbeat #37 (the leaf).
  2. Provide the merkle proof path -- the sibling hashes needed to reconstruct the root.
  3. The verifier hashes heartbeat #37, then combines with each sibling hash up the tree, arriving at the root.
  4. 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:

E0 ← E1 ← E2 ← E3 ← ... ← En

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.