# Copyright (c) 2026 Tiago Pinto # SPDX-License-Identifier: MIT # # Independent producer-side implementation of # draft-fassbender-scitt-time-anchor-03, written from the published # Internet-Draft alone. See README.md and CONSTRUCTION.md. """verify.py: VERIFY-ANCHOR (profile Section 3.1), all eleven steps. Returns exactly one of {valid, invalid, unverifiable} (Section 3.3; [ANCHORING] Section 5 forbids intermediate results). The algorithm is header-only: outside the bundle, nothing but block headers is fetched, from the implementer's own node (node_rpc), which that node validated. That is the condition without which valid cannot be returned (Section 3.1, note on header sources). Inline comments below are in Portuguese, the implementer's working language. """ import hashlib import ots_wire from node_rpc import header_merkle_root, header_ntime MIN_CONFIRMATIONS = 6 # §3.1 passo 9 def strip_prefix(s: str) -> str: return s[7:] if s.startswith('sha256:') else s def verify_anchor(artifact_bytes: bytes, proof_bundle: dict, rpc) -> dict: """VERIFY-ANCHOR(artifact_bytes, proof_bundle). proof_bundle: {'ots_proof': bytes, 'claimed_hash': str, 'origin_id': opcional, 'bitcoin_block_height': opcional} (CDDL do §2.4.3; os dois opcionais não são usados na verificação.) Devolve {'result': ..., e com valid: 'block_height_H', 'block_time_RWCP'}. """ # 1 — RECOMPUTE HASH (nunca confiar no claimed_hash) computed_hash = hashlib.sha256(artifact_bytes).digest() # 2 — COMPARE HASH if computed_hash.hex() != strip_prefix(proof_bundle['claimed_hash']): return {'result': 'invalid', 'step': 2} # 3 — PARSE OTS PROOF try: parsed = ots_wire.parse(proof_bundle['ots_proof']) except ots_wire.WireError: return {'result': 'invalid', 'step': 3} if parsed.msg != computed_hash: return {'result': 'invalid', 'step': 3} # 4 — CHECK PROOF STATUS btc = parsed.bitcoin_branches() if not btc: return {'result': 'unverifiable', 'step': 4, 'why': 'só compromisso de calendário; ainda não está no ledger'} # 5-8 — REPLAY, ALTURAS, HEADERS, CLASSIFICAR E COMBINAR branches = [] for ops, att in btc: digest = parsed.replay(ops, computed_hash) # 5 (None = unsupported) H = att.height # 6 header = rpc.fetch_block_header_by_height(H) # 7 (None = unavailable) if digest is None: state = 'unsupported' elif header is None: state = 'unavailable' elif digest == header_merkle_root(header): state = 'verified' else: state = 'mismatched' branches.append({'H': H, 'header': header, 'state': state}) if all(b['header'] is None for b in branches): # 7: ledger indisponível return {'result': 'unverifiable', 'step': 7} verified = [b for b in branches if b['state'] == 'verified'] if not verified: # 8 if any(b['state'] in ('unavailable', 'unsupported') for b in branches): return {'result': 'unverifiable', 'step': 8} return {'result': 'invalid', 'step': 8} best = min(verified, key=lambda b: b['H']) # altura mais baixa: bound mais apertado # 9 — CHECK CONFIRMATION DEPTH k = rpc.confirmations_on(best['H']) if k is None or k < MIN_CONFIRMATIONS: return {'result': 'unverifiable', 'step': 9, 'k': k} # 10 — DERIVE THE TEMPORAL VALUES block_height_H = best['H'] # normativo (§2.6.1) block_time_RWCP = header_ntime(best['header']) # informativo (§1.3) # 11 — valid return {'result': 'valid', 'block_height_H': block_height_H, 'block_time_RWCP': block_time_RWCP, 'confirmations': k}