cosmicledger-verifiable-off.../cosmic_ledger/delta.py

206 lines
7.7 KiB
Python

import hashlib
import json
from typing import List
def _hash_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def merkle_root(digests: List[str]) -> str:
if not digests:
return ""
level = [bytes.fromhex(d) for d in digests]
while len(level) > 1:
next_level = []
for i in range(0, len(level), 2):
left = level[i]
right = level[i+1] if i+1 < len(level) else level[i]
next_level.append(hashlib.sha256(left + right).digest())
level = next_level
return level[0].hex()
def merkle_path_for_index(digests: List[str], index: int) -> dict:
"""Compute Merkle root and the Merkle path for a given leaf index.
Returns a dict with keys:
- root: the Merkle root for the given leaves
- path: list of sibling digests (hex) from leaf to root
"""
if not digests:
return {"root": "", "path": []}
# convert hex digests to bytes for hashing
level = [bytes.fromhex(d) for d in digests]
idx = index
path = []
while len(level) > 1:
# determine sibling index for current level
sib_index = idx ^ 1 # toggle last bit -> sibling index
if 0 <= sib_index < len(level):
path.append(level[sib_index].hex())
else:
# no sibling, duplicate the current node
path.append(level[idx].hex())
# build next level
next_level = []
for i in range(0, len(level), 2):
left = level[i]
right = level[i+1] if i+1 < len(level) else level[i]
next_level.append(hashlib.sha256(left + right).digest())
level = next_level
idx = idx // 2
root = level[0].hex()
return {"root": root, "path": path}
def verify_delta_root(delta_entries: List[dict]) -> bool:
"""Verify that a sequence of delta entries shares a consistent Merkle root.
Each delta entry is expected to contain a top-level "digest" field, which
is the SHA-256 digest of the serialized entry payload. The function computes
the Merkle root over all provided digests and ensures that every delta entry
that carries a "delta_root" field agrees with the computed root. If no
delta_root is present, the function returns True once a root can be derived
from the available digests.
This helper is intended for cross-node verification when exchanging delta blocks.
"""
if not delta_entries:
return True
# Collect all available digests in the delta entries
digests = [e.get("digest") for e in delta_entries if e.get("digest") is not None]
if not digests:
return True
computed_root = merkle_root(digests)
# If entries provide an explicit delta_root, ensure consistency across them
for e in delta_entries:
explicit = e.get("delta_root")
if explicit is not None and explicit != computed_root:
return False
return True
def verify_inclusion(leaf_digest: str, merkle_path: List[str], root: str, index: int) -> bool:
"""Verify that a leaf digest is included in a Merkle tree with given root.
leaf_digest: hex string of the leaf digest
merkle_path: list of sibling digests (hex) from leaf up to, but not including, the root
root: expected Merkle root (hex)
index: the zero-based index of the leaf within the original leaves array
This mirrors the path generation in merkle_path_for_index: when a sibling is
missing the leaf is duplicated. We reconstruct the hash up to the root and
compare with the provided root.
"""
if not leaf_digest:
return False
try:
node = bytes.fromhex(leaf_digest)
except Exception:
return False
idx = index
for sib_hex in merkle_path:
try:
sib = bytes.fromhex(sib_hex)
except Exception:
return False
if idx % 2 == 0:
# current node is left child
node = hashlib.sha256(node + sib).digest()
else:
# current node is right child
node = hashlib.sha256(sib + node).digest()
idx = idx // 2
return node.hex() == (root or "")
class DeltaLog:
def __init__(self):
self.entries = [] # each entry is a dict with digest and payload
self.anchor = None # optional cloud/ground anchor for global verifiability
# Checkpoints store archived roots and the set of digests included at that time
# Each checkpoint is a dict: {"root": <hex>, "index": <exclusive_index>, "digests": [<hex>, ...]}
self.checkpoints = []
def add_entry(self, entry: dict) -> str:
# entry must be serializable, and we store a digest for Merkle
payload_bytes = json.dumps(entry, sort_keys=True).encode('utf-8')
digest = hashlib.sha256(payload_bytes).hexdigest()
self.entries.append({
"digest": digest,
"payload": entry,
})
return digest
def anchor_root(self, anchor: str) -> None:
"""Record an optional anchor for the current delta log.
This does not alter existing entries; it simply stores a reference
to a trusted anchor (e.g., ground control) to tie the local log to
an external verifiable state.
"""
self.anchor = anchor
def delta_from_index(self, index: int) -> List[dict]:
# Return full LedgerEntry dictionaries augmented with Merkle proofs.
# This enables recipients to reconstruct and verify the delta with
# compact proofs, while preserving compatibility with existing code.
leaves = [e["digest"] for e in self.entries]
result = []
for i in range(index, len(self.entries)):
entry = self.entries[i]
# Compute Merkle path for this leaf with respect to all leaves
path_info = merkle_path_for_index(leaves, i)
# The payload is the LedgerEntry dict previously stored in 'payload'
full_entry = entry["payload"].copy()
# Attach meta for verification without altering the payload structure
full_entry.update({
"digest": entry["digest"],
"delta_root": path_info["root"],
"merkle_path": path_info["path"],
})
result.append(full_entry)
return result
def root(self) -> str:
digests = [e["digest"] for e in self.entries]
return merkle_root(digests)
def checkpoint(self, prune_before_index: int = 0) -> dict:
"""Create a checkpoint for entries up to prune_before_index (exclusive) and optionally prune them.
The checkpoint records the Merkle root over the archived leaves and the list of archived digests.
It then removes those entries from the active entries list so storage can be reclaimed.
Returns the checkpoint dict.
"""
if prune_before_index <= 0:
# nothing to archive
cp_digests = []
cp_root = ""
else:
cp_digests = [e["digest"] for e in self.entries[:prune_before_index]]
cp_root = merkle_root(cp_digests)
checkpoint = {
"root": cp_root,
"index": prune_before_index,
"digests": cp_digests,
}
# store checkpoint
self.checkpoints.append(checkpoint)
# prune entries up to prune_before_index
if prune_before_index > 0:
self.entries = self.entries[prune_before_index:]
return checkpoint
def has_digest(self, digest: str) -> bool:
"""Return True if digest is present in active entries or archived checkpoints."""
for e in self.entries:
if e.get("digest") == digest:
return True
for cp in self.checkpoints:
if digest in cp.get("digests", []):
return True
return False