Skip to content

Commit 60b2124

Browse files
hyperpolymathclaude
andcommitted
proof(V7): provenance chain immutability — hash chain + monotonic timestamps
Agda 2.8.0 proof (no postulate violations): - HashChain predicate (LIFO prepend order, newest-first) - chain-tail-valid, chain-head-consistent, chain-link, chain-ts-head-ge - same-hash-same-fields via hash-injective (collision-resistance postulate) - tamper-detected: content change → contentHash change - chain-tamper-breaks-link: tampered predecessor breaks successor link - timestamps-non-decreasing: oldest ≤ newest per hc-cons - genesis-unique: only oldest record has genesisHash as parent - chain-extend: appending a valid record preserves HashChain Closes REQUIREMENTS-MASTER.md V7. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0639514 commit 60b2124

1 file changed

Lines changed: 245 additions & 0 deletions

File tree

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
-- SPDX-License-Identifier: PMPL-1.0-or-later
2+
-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
--
4+
-- ProvenanceChain.agda — Formal proof of provenance chain immutability (V7).
5+
--
6+
-- REQUIREMENTS-MASTER.md: V7 | Provenance chain immutability (hash chain, monotonic timestamps) | SEC | Ag | P1
7+
--
8+
-- Models rust-core/verisim-provenance/src/lib.rs:
9+
-- - ProvenanceRecord has: content, timestamp, parentHash, actorId, contentHash
10+
-- - Hash chain: parentHash[i+1] = contentHash[i] (each record links to its predecessor)
11+
-- - First record has parentHash = genesisHash (the "0"*64 sentinel)
12+
--
13+
-- List representation: PREPEND order — the HEAD of the list is the NEWEST record.
14+
-- This makes chain extension (hc-cons) structurally natural.
15+
--
16+
-- Properties proved:
17+
-- 1. HashChain predicate — well-formedness of a provenance chain
18+
-- 2. chain-tail-valid — tail of valid chain is valid
19+
-- 3. chain-head-consistent — head is self-consistent
20+
-- 4. chain-link — parentHash of head links to contentHash of predecessor
21+
-- 5. tamper-detected — changing content changes contentHash (by hash injectivity)
22+
-- 6. chain-tamper-breaks-link — a tampered record breaks the link from its successor
23+
-- 7. timestamps-non-decreasing — timestamps grow along the chain (oldest last)
24+
-- 8. genesis-unique — only the tail (oldest) record has genesisHash as parent
25+
-- 9. chain-extend — prepending a new record with a valid link extends the chain
26+
27+
module ProvenanceChain where
28+
29+
open import Data.Nat using (ℕ; zero; suc; _≤_; _<_; z≤n; s≤s)
30+
open import Data.Nat.Properties using (≤-refl; ≤-trans; n<1+n; <⇒≤)
31+
open import Data.List using (List; []; _∷_; length)
32+
open import Data.Product using (_×_; _,_; proj₁; proj₂)
33+
open import Data.Empty using (⊥; ⊥-elim)
34+
open import Relation.Binary.PropositionalEquality
35+
using (_≡_; refl; sym; trans; cong; _≢_)
36+
open import Relation.Nullary using (¬_)
37+
38+
------------------------------------------------------------------------
39+
-- Section 1: Abstract hash model
40+
------------------------------------------------------------------------
41+
42+
-- Hashes are abstracted as ℕ.
43+
Hash : Set
44+
Hash =
45+
46+
-- Abstract hash function for a provenance record's fields.
47+
-- content × timestamp × parentHash × actorId → contentHash
48+
postulate
49+
hashRecord : Hash Hash Hash Hash
50+
-- Collision resistance: equal outputs imply equal inputs.
51+
hash-injective : {c₁ t₁ p₁ a₁ c₂ t₂ p₂ a₂}
52+
hashRecord c₁ t₁ p₁ a₁ ≡ hashRecord c₂ t₂ p₂ a₂
53+
c₁ ≡ c₂ × t₁ ≡ t₂ × p₁ ≡ p₂ × a₁ ≡ a₂
54+
-- hashRecord never returns the genesis sentinel.
55+
-- Holds in Rust because SHA-256 of non-empty input ≠ "0"*64.
56+
hashRecord-not-genesis : (c t p a : Hash) hashRecord c t p a ≢ 0
57+
58+
-- Genesis parent hash: sentinel for the oldest record's parentHash.
59+
genesisHash : Hash
60+
genesisHash = 0
61+
62+
------------------------------------------------------------------------
63+
-- Section 2: Provenance record model
64+
------------------------------------------------------------------------
65+
66+
record ProvenanceRecord : Set where
67+
constructor mkRecord
68+
field
69+
content : Hash
70+
timestamp :
71+
parentHash : Hash
72+
actorId : Hash
73+
contentHash : Hash -- = hashRecord content timestamp parentHash actorId
74+
75+
-- A record is self-consistent if its contentHash is correctly computed.
76+
SelfConsistent : ProvenanceRecord Set
77+
SelfConsistent r =
78+
ProvenanceRecord.contentHash r ≡
79+
hashRecord
80+
(ProvenanceRecord.content r)
81+
(ProvenanceRecord.timestamp r)
82+
(ProvenanceRecord.parentHash r)
83+
(ProvenanceRecord.actorId r)
84+
85+
------------------------------------------------------------------------
86+
-- Section 3: Hash chain predicate
87+
--
88+
-- The list is stored NEWEST-FIRST (prepend order).
89+
-- HashChain (r_new ∷ r_old ∷ rs) means:
90+
-- - r_new is the most recently appended record
91+
-- - r_old is the record before r_new
92+
-- - r_new.parentHash = r_old.contentHash (link)
93+
-- - r_old.timestamp ≤ r_new.timestamp (monotone)
94+
------------------------------------------------------------------------
95+
96+
data HashChain : List ProvenanceRecord Set where
97+
98+
hc-nil : HashChain []
99+
100+
hc-first : {r}
101+
SelfConsistent r
102+
ProvenanceRecord.parentHash r ≡ genesisHash
103+
HashChain (r ∷ [])
104+
105+
-- Prepend r_new to an existing chain headed by r_old.
106+
hc-cons : {r_new r_old rs}
107+
HashChain (r_old ∷ rs) -- existing chain
108+
SelfConsistent r_new
109+
ProvenanceRecord.parentHash r_new ≡ ProvenanceRecord.contentHash r_old
110+
ProvenanceRecord.timestamp r_old ≤ ProvenanceRecord.timestamp r_new
111+
HashChain (r_new ∷ r_old ∷ rs) -- extended chain
112+
113+
------------------------------------------------------------------------
114+
-- Section 4: Structural lemmas
115+
------------------------------------------------------------------------
116+
117+
-- V7 — LEMMA 1: The tail of a valid chain is valid.
118+
-- (Easy: hc-cons's first argument is exactly the tail chain.)
119+
chain-tail-valid : {r rs}
120+
HashChain (r ∷ rs)
121+
HashChain rs
122+
chain-tail-valid (hc-first _ _) = hc-nil
123+
chain-tail-valid (hc-cons hc_tail _ _ _) = hc_tail
124+
125+
-- V7 — LEMMA 2: The head of a valid chain is self-consistent.
126+
chain-head-consistent : {r rs}
127+
HashChain (r ∷ rs)
128+
SelfConsistent r
129+
chain-head-consistent (hc-first sc _) = sc
130+
chain-head-consistent (hc-cons _ sc _ _) = sc
131+
132+
-- V7 — LEMMA 3: The head's parentHash equals its predecessor's contentHash.
133+
chain-link : {r_new r_old rs}
134+
HashChain (r_new ∷ r_old ∷ rs)
135+
ProvenanceRecord.parentHash r_new ≡ ProvenanceRecord.contentHash r_old
136+
chain-link (hc-cons _ _ link _) = link
137+
138+
-- V7 — LEMMA 4: Timestamps are non-decreasing head-to-predecessor.
139+
-- Since the list is newest-first, the head timestamp is ≥ the second element's.
140+
chain-ts-head-ge : {r_new r_old rs}
141+
HashChain (r_new ∷ r_old ∷ rs)
142+
ProvenanceRecord.timestamp r_old ≤ ProvenanceRecord.timestamp r_new
143+
chain-ts-head-ge (hc-cons _ _ _ ts) = ts
144+
145+
------------------------------------------------------------------------
146+
-- Section 5: Immutability theorems
147+
------------------------------------------------------------------------
148+
149+
-- V7 — THEOREM 1: Two self-consistent records with the same contentHash
150+
-- have identical field values (injectivity of hashRecord).
151+
same-hash-same-fields :
152+
{r₁ r₂ : ProvenanceRecord}
153+
SelfConsistent r₁
154+
SelfConsistent r₂
155+
ProvenanceRecord.contentHash r₁ ≡ ProvenanceRecord.contentHash r₂
156+
ProvenanceRecord.content r₁ ≡ ProvenanceRecord.content r₂
157+
× ProvenanceRecord.timestamp r₁ ≡ ProvenanceRecord.timestamp r₂
158+
× ProvenanceRecord.parentHash r₁ ≡ ProvenanceRecord.parentHash r₂
159+
× ProvenanceRecord.actorId r₁ ≡ ProvenanceRecord.actorId r₂
160+
same-hash-same-fields sc₁ sc₂ heq =
161+
hash-injective (trans (sym sc₁) (trans heq sc₂))
162+
163+
-- V7 — THEOREM 2: Changing the content field changes the contentHash.
164+
tamper-detected :
165+
{r r' : ProvenanceRecord}
166+
SelfConsistent r
167+
SelfConsistent r'
168+
ProvenanceRecord.content r ≢ ProvenanceRecord.content r'
169+
ProvenanceRecord.contentHash r ≢ ProvenanceRecord.contentHash r'
170+
tamper-detected sc sc' hne heq =
171+
hne (proj₁ (same-hash-same-fields sc sc' heq))
172+
173+
-- V7 — THEOREM 3: Replacing the predecessor r_old with a tampered r_old'
174+
-- (different content) causes the head r_new's parentHash to no longer match.
175+
chain-tamper-breaks-link :
176+
{r_new r_old : ProvenanceRecord} {rs : List ProvenanceRecord}
177+
HashChain (r_new ∷ r_old ∷ rs)
178+
(r_old' : ProvenanceRecord)
179+
SelfConsistent r_old'
180+
ProvenanceRecord.content r_old ≢ ProvenanceRecord.content r_old'
181+
ProvenanceRecord.parentHash r_new ≢ ProvenanceRecord.contentHash r_old'
182+
chain-tamper-breaks-link hc r_old' sc' hContentNe =
183+
let hc-old : SelfConsistent _
184+
hc-old = chain-head-consistent (chain-tail-valid hc)
185+
hLink = chain-link hc
186+
hHashNe = tamper-detected hc-old sc' hContentNe
187+
in λ link' hHashNe (trans (sym hLink) link')
188+
189+
------------------------------------------------------------------------
190+
-- Section 6: Timestamp monotonicity
191+
------------------------------------------------------------------------
192+
193+
-- Extract timestamps in newest-first order.
194+
chainTimestamps : List ProvenanceRecord List ℕ
195+
chainTimestamps [] = []
196+
chainTimestamps (r ∷ rs) = ProvenanceRecord.timestamp r ∷ chainTimestamps rs
197+
198+
-- V7 — THEOREM 4: Timestamps in a valid chain are non-decreasing from
199+
-- oldest (tail) to newest (head). Equivalently, chainTimestamps is a
200+
-- non-increasing sequence (newest first → values are ≥ previous).
201+
--
202+
-- We prove the pointwise version: for any adjacent pair in the chain,
203+
-- the older record's timestamp ≤ the newer record's timestamp.
204+
timestamps-non-decreasing :
205+
{r_new r_old rs}
206+
HashChain (r_new ∷ r_old ∷ rs)
207+
ProvenanceRecord.timestamp r_old ≤ ProvenanceRecord.timestamp r_new
208+
timestamps-non-decreasing = chain-ts-head-ge
209+
210+
------------------------------------------------------------------------
211+
-- Section 7: Genesis uniqueness
212+
------------------------------------------------------------------------
213+
214+
-- V7 — THEOREM 5: Only the tail (oldest) record has genesisHash as parentHash.
215+
-- Any non-first record's parentHash equals the contentHash of its predecessor,
216+
-- and contentHash is produced by hashRecord, which never returns genesisHash.
217+
genesis-unique :
218+
{r_new r_old rs}
219+
HashChain (r_new ∷ r_old ∷ rs)
220+
ProvenanceRecord.parentHash r_new ≢ genesisHash
221+
genesis-unique {r_new} {r_old} hc hEq =
222+
let hLink = chain-link hc
223+
sc_old = chain-head-consistent (chain-tail-valid hc)
224+
in hashRecord-not-genesis
225+
(ProvenanceRecord.content r_old)
226+
(ProvenanceRecord.timestamp r_old)
227+
(ProvenanceRecord.parentHash r_old)
228+
(ProvenanceRecord.actorId r_old)
229+
(trans (sym sc_old) (trans (sym hLink) hEq))
230+
231+
------------------------------------------------------------------------
232+
-- Section 8: Chain extension
233+
------------------------------------------------------------------------
234+
235+
-- V7 — THEOREM 6: Prepending a new well-formed record extends the chain.
236+
-- The caller supplies the link proof and the timestamp proof.
237+
chain-extend :
238+
{r_old rs}
239+
HashChain (r_old ∷ rs)
240+
(r_new : ProvenanceRecord)
241+
SelfConsistent r_new
242+
ProvenanceRecord.parentHash r_new ≡ ProvenanceRecord.contentHash r_old
243+
ProvenanceRecord.timestamp r_old ≤ ProvenanceRecord.timestamp r_new
244+
HashChain (r_new ∷ r_old ∷ rs)
245+
chain-extend hc r_new sc link ts = hc-cons hc sc link ts

0 commit comments

Comments
 (0)