Ed25519 + Merkle Tree + UUIDv7 = Building Tamper-Proof Decision Logs

Published: (December 13, 2025 at 02:58 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

TL;DR – Combine Ed25519 signatures, Merkle trees, and UUIDv7 identifiers to build an immutable, tamper‑evident audit trail for AI and algorithmic systems. A full Python implementation is provided below.

The Problem: AI Decisions at the Speed of Light

Your trading algorithm made 10 000 decisions in the last second.
Your ML model approved 500 loan applications.
Your autonomous system executed a critical maneuver.

Now someone asks: “What exactly happened at 14:32:07.847?”

You need to prove:

  • What decision was made.
  • When it was made (with cryptographic certainty).
  • That nothing was altered after the fact.

Traditional logging fails: database records can be edited, timestamps forged, and log files tampered. When AI systems operate faster than human comprehension, we need cryptographic proof.

The Three Pillars

PrimitivePurposeStandard
UUIDv7Time‑ordered unique identifiersRFC 9562
Hash ChainTamper‑evident linkingSHA‑256
Ed25519Digital signaturesRFC 8032
Merkle TreeEfficient batch verificationRFC 6962

Each solves a specific problem; together they form an unbreakable chain of evidence.

Pillar 1: UUIDv7 — Time‑Ordered Identity

UUIDv7 (RFC 9562) embeds a 48‑bit Unix timestamp in milliseconds directly into the identifier, giving:

  • Lexicographic sorting = chronological sorting
  • Embedded temporal proof independent of any separate timestamp field
  • Uniqueness without central coordination
# uuidv7_example.py
import uuid
import time

def generate_uuidv7() -> str:
    """Generate a UUIDv7 identifier with embedded timestamp."""
    # Current time in milliseconds
    timestamp_ms = int(time.time() * 1000)

    # UUIDv7 structure:
    # - 48 bits: timestamp (ms)
    # - 4 bits: version (7)
    # - 12 bits: random
    # - 2 bits: variant
    # - 62 bits: random

    # Python 3.12+ includes uuid7; for earlier versions use the `uuid7` package
    return str(uuid.uuid.7())
    # Example output: "019234ab-cdef-7000-8123-456789abcdef"
    #                  ^^^^^^^^ timestamp embedded here
# uuidv7_utils.py
def extract_timestamp_from_uuidv7(uuid_str: str) -> int:
    """Extract the embedded millisecond timestamp from a UUIDv7."""
    uuid_hex = uuid_str.replace("-", "")
    # First 48 bits (12 hex chars) contain the timestamp
    return int(uuid_hex[:12], 16)

def detect_timestamp_anomaly(event_id: str, claimed_timestamp_ms: int,
                             threshold_ms: int = 5000) -> bool:
    """
    Return True if the claimed timestamp deviates from the UUIDv7 embedded
    timestamp by more than `threshold_ms`.
    """
    embedded_ts = extract_timestamp_from_uuidv7(event_id)
    drift = abs(embedded_ts - claimed_timestamp_ms)
    return drift > threshold_ms

Why this matters – If an event claims to have occurred at time T but the UUIDv7 timestamp says T + 5 minutes, the discrepancy is cryptographically evident.

Pillar 2: Hash Chains — Tamper‑Evident Linking

Each event stores the hash of the previous event, forming an immutable chain:

Event₁ → H(Event₁) → Event₂ → H(Event₂) → Event₃ → …
          ↓                     ↓
      prev_hash            prev_hash

If any earlier event is altered, its hash changes, breaking the link to all subsequent events.

# hash_chain.py
import hashlib
import json

# Genesis hash: 64 zeros (256 bits)
GENESIS_HASH = "0" * 64

def canonicalize_json(obj: dict) -> str:
    """
    RFC 8785 JSON Canonicalization:
    - Lexicographic key ordering
    - No whitespace
    - Deterministic number formatting
    """
    return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)

def compute_event_hash(header: dict, payload: dict, prev_hash: str,
                       algo: str = "sha256") -> str:
    """
    Compute event hash as:
    H( canonical(header) || canonical(payload) || prev_hash )
    """
    data = (
        canonicalize_json(header) +
        canonicalize_json(payload) +
        prev_hash
    ).encode("utf-8")

    if algo == "sha256":
        return hashlib.sha256(data).hexdigest()
    elif algo == "sha3_256":
        return hashlib.sha3_256(data).hexdigest()
    else:
        raise ValueError(f"Unsupported algorithm: {algo}")
# chain_validation.py
def validate_chain(events: list[dict]) -> tuple[bool, str]:
    """
    Validate the integrity of an event chain.
    Returns (is_valid, message).
    """
    if not events:
        return True, "Empty chain"

    # First event must reference the genesis hash
    if events[0]["security"]["prev_hash"] != GENESIS_HASH:
        return False, "Genesis event has incorrect prev_hash"

    # Verify each subsequent event
    for i in range(1, len(events)):
        cur = events[i]
        prev = events[i - 1]

        # Expected hash of the previous event
        expected_prev_hash = compute_event_hash(
            prev["header"], prev["payload"], prev["security"]["prev_hash"]
        )

        # Linkage check
        if cur["security"]["prev_hash"] != expected_prev_hash:
            return False, f"Chain broken at event {i}: prev_hash mismatch"

        # Verify current event's own hash
        computed_hash = compute_event_hash(
            cur["header"], cur["payload"], cur["security"]["prev_hash"]
        )
        if cur["security"]["event_hash"] != computed_hash:
            return False, f"Event {i} hash mismatch: content was modified"

    return True, "Chain valid"

Pillar 3: Ed25519 Signatures — Authenticity and Non‑Repudiation

Hash chains guarantee integrity but not who created a record. Ed25519 (RFC 8032) provides fast, deterministic, 256‑bit security signatures.

# ed25519_signer.py
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
    Ed25519PrivateKey, Ed25519PublicKey
)
from cryptography.hazmat.primitives import serialization

class EventSigner:
    """Ed25519 signer for VCP events."""

    def __init__(self, private_key: Ed25519PrivateKey = None):
        self.private_key = private_key or Ed25519PrivateKey.generate()
        self.public_key = self.private_key.public_key()

    def get_public_key_hex(self) -> str:
        """Export public key as a hex string."""
        pk_bytes = self.public_key.public_bytes(
            encoding=serialization.Encoding.Raw,
            format=serialization.PublicFormat.Raw
        )
        return pk_bytes.hex()

    def sign_event(self, event_hash: str) -> str:
        """Sign an event hash; returns hex‑encoded signature."""
        signature = self.private_key.sign(bytes.fromhex(event_hash))
        return signature.hex()

    @staticmethod
    def verify_signature(public_key_hex: str, event_hash: str,
                         signature_hex: str) -> bool:
        """Verify a signature; returns False on failure."""
        try:
            public_key = Ed25519PublicKey.from_public_bytes(
                bytes.fromhex(public_key_hex)
            )
            public_key.verify(
                bytes.fromhex(signature_hex),
                bytes.fromhex(event_hash)
            )
            return True
        except Exception:
            return False

Putting It All Together

  1. Generate a UUIDv7 for the event – this supplies a cryptographic timestamp.
  2. Create the event payload (header, data, etc.) and compute its hash, chaining to the previous event’s hash.
  3. Sign the event hash with an Ed25519 private key; store the signature and the public‑key identifier alongside the event.
  4. Optionally batch many events into a Merkle tree (RFC 6962) for efficient collective verification.

The resulting log provides:

  • Chronological ordering via UUIDv7.
  • Tamper‑evidence via hash chaining (and optionally Merkle proofs).
  • Authenticity & non‑repudiation via Ed25519 signatures.

This combination yields a robust, tamper‑proof decision log suitable for high‑throughput AI and autonomous systems.

Back to Blog

Related posts

Read more »