From 45bc190445121a20825ab55898d92c7aa43ff153 Mon Sep 17 00:00:00 2001 From: agent-56a7678c6cd71659 Date: Thu, 30 Apr 2026 08:43:31 +0200 Subject: [PATCH] build(agent): jabba#56a767 iteration --- gravityweave/pcp.py | 37 +++++++++++++++++++++++++++---------- tests/test_pcp.py | 15 +++++++++++++++ 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/gravityweave/pcp.py b/gravityweave/pcp.py index 1defed1..91e29dd 100644 --- a/gravityweave/pcp.py +++ b/gravityweave/pcp.py @@ -98,17 +98,34 @@ def verify_inclusion(chain_head_hash: str, proof_hashes: List[str], chain_map: O if chain_map is None: # best-effort: we have only hashes; accept if tip matches return True - # verify backward links using chain_map + # verify forward links using chain_map # proof_hashes = [h_target, h_next, ..., h_head] + # For each adjacent pair, the later entry's prev_hash must equal the + # earlier entry's hash. We consult chain_map which maps hash->entry. for i in range(len(proof_hashes) - 1): - h = proof_hashes[i] - next_h = proof_hashes[i + 1] - entry = chain_map.get(h) - if entry is None: + h_curr = proof_hashes[i] + h_next = proof_hashes[i + 1] + # ensure the provided map contains the entry and that the entry's + # recorded hash matches the canonical hash of its payload. This + # prevents silent tampering where prev_hash is changed but hash left + # un-updated. + next_entry = chain_map.get(h_next) + if next_entry is None: + return False + # recompute the entry hash and ensure it matches the expected key + try: + # debug: show object id + # print(f"verify_inclusion: next_entry id={id(next_entry)}") + recomputed = _entry_hash(next_entry) + except Exception: + print(f"verify_inclusion: failed to recompute hash for {h_next}") + return False + if recomputed != h_next: + return False + # next_entry.prev_hash should point to current hash + prev = next_entry.get("prev_hash", "") + # debug print + # print(f"verify link: next={h_next} prev={prev} expected={h_curr}") + if prev != h_curr: return False - if entry.get("prev_hash", "") != (chain_map.get(next_h, {}).get("prev_hash", entry.get("prev_hash", "")) and None): - # We cannot reliably reconstruct prev pointers from hashes alone - # so this strict check is left as a placeholder. For now we require - # that next hash exists in the map (sanity) and tip matches. - pass return True diff --git a/tests/test_pcp.py b/tests/test_pcp.py index daff06f..db81492 100644 --- a/tests/test_pcp.py +++ b/tests/test_pcp.py @@ -23,3 +23,18 @@ def test_inclusion_proof_and_head(): assert proof[-1] == head # verify inclusion using head-only verification (best-effort) assert verify_inclusion(head, proof) + + +def test_verify_inclusion_with_chain_map(): + ops = [("opA", "X"), ("opB", "Y"), ("opC", "Z")] + chain = build_chain(ops) + head = head_hash(chain) + target = chain[0] + proof = inclusion_proof(chain, target_entry_id=target["entry_id"]) + # build chain_map: hash -> entry + chain_map = {e["hash"]: e for e in chain} + # verification should succeed when supplying chain_map + assert verify_inclusion(head, proof, chain_map=chain_map) + # build a clean chain_map and verify inclusion with the map provided + chain_map = {e["hash"]: e for e in chain} + assert verify_inclusion(head, proof, chain_map=chain_map)