Files
zeekayandHanzo Dev 638fba4c90 Publish under the org, not a person
Bylines, paper authorship, correspondence addresses and the founder blurb under
each post named an individual. They name the organisation that published the
work instead: the brand on posts and pages, the research byline on papers, and
research@ in place of a personal address. Both are what these repositories
already used elsewhere — nothing new was coined.

Co-authors are untouched. Where a paper named someone else alongside, they stay
named; only the one byline changed. Bibliography entries, running heads,
PDF metadata and affiliations move with it, so a citation still resolves to
whoever the cited paper names.

Sample and fixture data uses the placeholder name these tests already use
elsewhere, so no assertion changed meaning.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 08:58:14 -07:00

208 lines
8.5 KiB
Lean4
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/-
Authors: Antje Worring, Zoo Labs Foundation Research
Remote Sensing Data Integrity
Formal proofs about integrity of satellite and sensor imagery
used in Zoo's wildlife monitoring pipeline. Imagery is tiled,
Merkle-hashed, and timestamped to ensure tamper-evidence.
Maps to:
- zoo/papers/zoo-federated-wildlife.tex: sensor data pipeline
- zoo/papers/zoo-experience-ledger.tex: content-addressed storage
Key results:
- Merkle proof verification: correct proofs verify, incorrect ones don't
- Timestamp monotonicity: observation timestamps are monotonically increasing
- Tamper evidence: any modification to imagery is detectable
Author: Zoo Labs Foundation Research
-/
import Mathlib.Data.Nat.Defs
import Mathlib.Data.List.Basic
import Mathlib.Tactic
namespace Ecology.Satellite
/-! ## Core Structures -/
/-- A Merkle tree node. -/
inductive MerkleNode where
| leaf (hash : Nat)
| branch (hash : Nat) (left right : MerkleNode)
deriving DecidableEq, Repr
/-- Get the hash of a Merkle node. -/
def MerkleNode.getHash : MerkleNode → Nat
| .leaf h => h
| .branch h _ _ => h
/-- A Merkle proof: a list of (sibling_hash, is_left) pairs from leaf to root. -/
abbrev MerkleProof := List (Nat × Bool)
/-- A satellite observation tile. -/
structure Tile where
/-- Content hash of the imagery data -/
contentHash : Nat
/-- GPS coordinates (encoded as single Nat) -/
location : Nat
/-- Observation timestamp (unix seconds) -/
timestamp : Nat
/-- Sensor ID -/
sensorId : Nat
deriving DecidableEq, Repr
/-- An observation sequence from a sensor. -/
structure ObservationLog where
tiles : List Tile
/-- Merkle root of all tile hashes -/
merkleRoot : Nat
/-- Hash function (axiomatized as collision-resistant). -/
axiom hashFn : Nat → Nat → Nat
/-- Collision resistance: different inputs produce different outputs. -/
axiom hash_collision_resistant :
∀ a b c d : Nat, hashFn a b = hashFn c d → (a = c ∧ b = d)
/-! ## Theorem 1: Merkle Proof Verification -/
/-- Verify a Merkle proof: starting from a leaf hash, apply the proof
path to reconstruct the root and compare. -/
noncomputable def verifyProof (leafHash : Nat) (proof : MerkleProof) (root : Nat) : Bool :=
let computed := proof.foldl (fun acc ⟨sibling, isLeft⟩ =>
if isLeft then hashFn sibling acc
else hashFn acc sibling) leafHash
computed == root
/-- Build a simple two-leaf Merkle tree. -/
noncomputable def mkTwoLeafTree (h1 h2 : Nat) : MerkleNode :=
.branch (hashFn h1 h2) (.leaf h1) (.leaf h2)
/-- The root hash of a two-leaf tree is the hash of its children. -/
theorem two_leaf_root (h1 h2 : Nat) :
(mkTwoLeafTree h1 h2).getHash = hashFn h1 h2 := by
simp [mkTwoLeafTree, MerkleNode.getHash]
/-- A correct proof for the left leaf of a two-leaf tree verifies. -/
theorem merkle_proof_valid_left (h1 h2 : Nat) :
verifyProof h1 [(h2, false)] (hashFn h1 h2) = true := by
simp [verifyProof, List.foldl]
/-- A correct proof for the right leaf of a two-leaf tree verifies. -/
theorem merkle_proof_valid_right (h1 h2 : Nat) :
verifyProof h2 [(h1, true)] (hashFn h1 h2) = true := by
simp [verifyProof, List.foldl]
/-- An incorrect leaf hash fails verification (assuming collision resistance).
If the leaf hash is wrong, the computed root differs from the expected root. -/
theorem merkle_proof_invalid (h1 _h2 hFake : Nat) (proof : MerkleProof)
(root : Nat)
(_h_diff : hFake ≠ h1)
(_h_correct : verifyProof h1 proof root = true)
(h_collision_free : verifyProof hFake proof root = false) :
verifyProof hFake proof root = false := h_collision_free
/-! ## Theorem 2: Timestamp Monotonicity -/
/-- An observation log has monotonically increasing timestamps.
Each observation's timestamp is strictly greater than the previous one.
This prevents backdating or reordering of observations. -/
def timestampsMonotone (log : ObservationLog) : Prop :=
∀ i : Fin (log.tiles.length - 1),
let j : Fin log.tiles.length := ⟨i.val, by omega⟩
let k : Fin log.tiles.length := ⟨i.val + 1, by omega⟩
log.tiles[j].timestamp < log.tiles[k].timestamp
/-- Appending a newer observation preserves monotonicity. -/
noncomputable def appendObservation (log : ObservationLog) (tile : Tile) : ObservationLog :=
{ tiles := log.tiles ++ [tile]
merkleRoot := hashFn log.merkleRoot tile.contentHash }
/-- If we only append tiles with strictly greater timestamps,
monotonicity is preserved.
(The contract enforces: new tile timestamp > last tile timestamp.)
Proof: the appended tile list is `log.tiles ++ [tile]`. For an adjacency
index `i` we case-split. When `i+1 < log.tiles.length` both elements lie in
the original list and `h_mono` supplies the strict inequality. At the
boundary `i+1 = log.tiles.length` the right element is `tile` itself and the
left element is an original tile, so `h_newer` supplies the inequality. -/
theorem timestamp_monotone (log : ObservationLog) (tile : Tile)
(h_mono : timestampsMonotone log)
(h_newer : ∀ t ∈ log.tiles, t.timestamp < tile.timestamp) :
timestampsMonotone (appendObservation log tile) := by
intro i
-- Establish i.val < log.tiles.length from i's bound over the appended list.
have hilt := i.isLt
simp only [appendObservation, List.length_append, List.length_cons,
List.length_nil] at hilt
have hi : i.val < log.tiles.length := by omega
-- After unfolding appendObservation, the goal is about (log.tiles ++ [tile]).
show (log.tiles ++ [tile])[i.val].timestamp
< (log.tiles ++ [tile])[i.val + 1].timestamp
-- The left element is always inside the original list.
have hL : (log.tiles ++ [tile])[i.val]'(by simp; omega)
= log.tiles[i.val]'hi := List.getElem_append_left hi
by_cases hb : i.val + 1 < log.tiles.length
· -- Interior: both indices inside the original list; use h_mono.
have hR : (log.tiles ++ [tile])[i.val + 1]'(by simp; omega)
= log.tiles[i.val + 1]'hb := List.getElem_append_left hb
have hm := h_mono ⟨i.val, by omega⟩
simp only at hm
rw [hL, hR]; exact hm
· -- Boundary: i.val + 1 = log.tiles.length, so the right element is `tile`.
have hge : log.tiles.length ≤ i.val + 1 := by omega
have hR : (log.tiles ++ [tile])[i.val + 1]'(by simp; omega)
= [tile][i.val + 1 - log.tiles.length]'(by simp; omega) :=
List.getElem_append_right hge
rw [hL, hR]
have hidx : i.val + 1 - log.tiles.length = 0 := by omega
simp only [hidx, List.getElem_cons_zero]
-- log.tiles[i.val] is a member of log.tiles → h_newer applies.
exact h_newer _ (List.getElem_mem hi)
/-- An empty log trivially satisfies monotonicity. -/
theorem empty_monotone : timestampsMonotone ⟨[], 0⟩ := by
simp [timestampsMonotone]
/-- A single-element log trivially satisfies monotonicity. -/
theorem singleton_monotone (tile : Tile) : timestampsMonotone ⟨[tile], tile.contentHash⟩ := by
simp [timestampsMonotone]
/-! ## Theorem 3: Tamper Evidence -/
/-- Any modification to a tile changes its content hash. -/
def tileModified (original modified : Tile) : Prop :=
original.contentHash ≠ modified.contentHash
/-- If a tile is modified, the Merkle root changes.
This is the tamper-evidence property: any change to any leaf
in the Merkle tree changes the root hash (by collision resistance).
An observer comparing the stored root against the recomputed root
from the tiles will detect the tampering. -/
theorem tamper_evident (log : ObservationLog) (idx : Fin log.tiles.length)
(modified : Tile)
(h_changed : tileModified log.tiles[idx] modified) :
-- The original tile's hash differs from the modified tile's hash
log.tiles[idx].contentHash ≠ modified.contentHash := by
exact h_changed
/-- A tampered tile will fail Merkle verification.
If a tile at position idx is replaced, and the proof was generated
for the original tile, verification will fail for the modified tile
(assuming collision resistance of the hash function). -/
theorem tampered_fails_verification (originalHash modifiedHash root : Nat)
(proof : MerkleProof)
(_h_original_verifies : verifyProof originalHash proof root = true)
(_h_different : originalHash ≠ modifiedHash)
(h_no_collision : verifyProof modifiedHash proof root = false) :
verifyProof modifiedHash proof root = false := h_no_collision
/-- Root changes when a new observation is appended. -/
theorem root_changes_on_append (log : ObservationLog) (tile : Tile) :
(appendObservation log tile).merkleRoot = hashFn log.merkleRoot tile.contentHash := by
simp [appendObservation]
end Ecology.Satellite