# 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. """tests_offline.py: full test battery; runs with no network and no node. Usage: python3 tests_offline.py [path to draft-fassbender-scitt-time-anchor-03.txt] With the draft present, also validates the four Appendix E.5 vectors (parse plus replay). Inline comments below are in Portuguese, the implementer's working language. """ import base64, copy, hashlib, itertools, re, sys import ots_wire, anchor, verify, upgrade, calendar_client FALHAS = [] def check(nome, cond): print(('OK ' if cond else 'FALHA ') + nome) if not cond: FALHAS.append(nome) ROOT_E2 = "b4d19ae256a6c3bceb27d1a31fbb3dd3e4574616ebe99a367d6ad7224b7e0749" MERKLE_E3 = "81383c9ce0d3174bcc6c352a0a821f9e5151e0bc608322ace6aeecccff3c43ef" H_E3 = 957289 E1 = [ ("a183f624efbbda7d6208b9b5a6f9b0c2c95d90382eb209c44a1131954071f30b","14ed82e243f4aff3f1794e6754bfae3b"), ("0df073a986e5e5141c96ec32303b8552ded255e4404d0aff996936a846b48cff","b0de8a5526115745c2801d65397db556"), ("d82bf84f17057c0e3d7aa6f249dfc2e80a877b910ce4ce14dc40e653fd5be40b","f6518213abf9ba8a92c3cb3bcdb82355"), ("94a86d117047f2af9a2f5635d0c530cc85fc23d20f5d4b42926b94abee7a06ec","2fb8ccfdfb881b481695a37deeced8e6"), ] DIGESTS_E5 = {1:"2c1ec4cbe4779d8d598b0f9b951d8d2b351b85ab1d9ad4fe0f897f0b8dd7fd97", 2:"dc630f6039890b988b2de9c51cf5b93915a24488ab69f05b3d3ed4957febb900", 3:"4e2a3ce0758ab8bef4b1e8311dea6e987b23a718d6eb40b6ed9f239e8cfe1f73", 4:"4905b48a6d26ed43a12e92c6a75930b2d36ace7889a5937c61de45f6753b450d"} # --- E.1/E.2 por SHA-256 puro --- leaves = [] ok = True for i,(eh,en) in enumerate(E1,1): h = hashlib.sha256(f"draft-fassbender-scitt-time-anchor-03 vector {i}".encode()).digest() ok = ok and h.hex()==eh leaf = hashlib.sha256(h+bytes.fromhex(en)).digest() leaves.append(leaf) n12 = hashlib.sha256(leaves[0]+leaves[1]).digest() n34 = hashlib.sha256(leaves[2]+leaves[3]).digest() root = hashlib.sha256(n12+n34).digest() check("E.1/E.2: hashes, folhas, nos e raiz (rota 1 do E.7)", ok and root.hex()==ROOT_E2) # --- E.5: parse + replay dos 4 vetores (precisa do draft) --- if len(sys.argv) > 1: txt = open(sys.argv[1]).read() parts = re.split(r'\*Vector (\d)\* \(1877 bytes; SHA-256 of the file:\s*\n\s*([0-9a-f]{64})\)', txt) for i in range(1, len(parts)-2, 3): n = int(parts[i]); body = re.split(r'\*Vector \d\*|E\.6\.', parts[i+2])[0] b64 = re.findall(r'^\s{3}([A-Za-z0-9+/=]+)\s*$', body, re.M) raw = base64.b64decode(''.join(b64)) h = hashlib.sha256(f"draft-fassbender-scitt-time-anchor-03 vector {n}".encode()).digest() p = ots_wire.parse(raw) opsb, att = p.bitcoin_branches()[0] check(f"E.5 vector {n}: digest do ficheiro", hashlib.sha256(raw).hexdigest()==DIGESTS_E5[n] and len(raw)==1877) check(f"E.5 vector {n}: msg, altura {H_E3}, replay->raiz Merkle, round-trip", p.msg==h and att.height==H_E3 and p.replay(opsb,h).hex()==MERKLE_E3 and p.serialize()==raw) else: print("aviso: sem o draft como argumento, os vetores E.5 nao sao validados") # --- BATCH-ANCHOR reproduz E.1/E.2 --- nonces = iter(bytes.fromhex(n) for _,n in E1) origins = [{'origin_id': f'o{i}', 'hash': h} for i,(h,_) in enumerate(E1)] capt = [] def fake_submit(server, r): capt.append(r); return ots_wire.Attestation.pending(server) proofs = anchor.batch_anchor(origins, {}, ["https://a.exemplo","https://b.exemplo"], fake_submit, random_bytes=lambda n: next(nonces)) check("BATCH-ANCHOR: raiz submetida == E.2", capt[0].hex()==ROOT_E2) check("BATCH-ANCHOR: caminhos replay->raiz", all(ots_wire.Proof('sha256',o['digest'],ots_wire.Node()).replay(o['path'],o['digest']).hex()==ROOT_E2 for o in origins)) check("provas pendentes: parse, 2 pending, 0 bitcoin, replay, round-trip", all((lambda p,b: p.msg==o['digest'] and len(p.pending_branches())==2 and not p.bitcoin_branches() and all(p.replay(ops,p.msg).hex()==ROOT_E2 for ops,_ in p.pending_branches()) and p.serialize()==b)(ots_wire.parse(b),b) for (_,b),o in zip(proofs,origins))) # --- lotes 1..9 com raiz independente --- def raiz_indep(ls): nivel=list(ls) while len(nivel)>1: nxt=[hashlib.sha256(nivel[i]+nivel[i+1]).digest() for i in range(0,len(nivel)-1,2)] if len(nivel)%2: nxt.append(nivel[-1]) nivel=nxt return nivel[0] ok = True for n in range(1,10): hs=[hashlib.sha256(f"l{n}a{i}".encode()).digest() for i in range(n)] ns=[hashlib.sha256(f"n{n}{i}".encode()).digest()[:16] for i in range(n)] it=iter(ns); c=[] def fs(s,r,c=c): c.append(r); return ots_wire.Attestation.pending(s) prfs=anchor.batch_anchor([{'origin_id':f'{n}-{i}','hash':h.hex()} for i,h in enumerate(hs)], {}, ["https://c.exemplo"], fs, random_bytes=lambda k: next(it)) folhas=[hashlib.sha256(h+non).digest() for h,non in zip(hs,ns)] ok = ok and c[0]==raiz_indep(folhas) and all( ots_wire.parse(b).replay(ots_wire.parse(b).pending_branches()[0][0],h)==raiz_indep(folhas) for (_,b),h in zip(prfs,hs)) check("lotes 1..9: raiz e replays contra recomputacao independente", ok) # --- VERIFY-ANCHOR e gates D.2 com no sintetico (usa vector 1 se presente) --- try: raw = open('vector1.ots','rb').read() except FileNotFoundError: raw = None if raw: def mk(root,t): return b'\x01\x00\x00\x00'+b'\x00'*32+root+t.to_bytes(4,'little')+b'\xff'*4+b'\x00'*4 class FakeRPC: def __init__(s,hd,tip): s.hd,s.tip=hd,tip def fetch_block_header_by_height(s,h): return s.hd.get(h) def confirmations_on(s,h): return None if s.tip is None or s.tip valid", verify.verify_anchor(art,bundle,FakeRPC({H_E3:mk(R,1)},H_E3+5))['result']=='valid') check("VERIFY: k=5 -> unverifiable (passo 9)", verify.verify_anchor(art,bundle,FakeRPC({H_E3:mk(R,1)},H_E3+4))['result']=='unverifiable') check("VERIFY: sem header -> unverifiable (passo 7)", verify.verify_anchor(art,bundle,FakeRPC({},None))['result']=='unverifiable') check("VERIFY: raiz errada -> invalid (passo 8)", verify.verify_anchor(art,bundle,FakeRPC({H_E3:mk(b'\xAA'*32,1)},H_E3+100))['result']=='invalid') check("VERIFY: artefacto mutado -> invalid (passo 2)", verify.verify_anchor(art+b'X',bundle,FakeRPC({H_E3:mk(R,1)},H_E3+5))['result']=='invalid') calendar_client_get = calendar_client.get_timestamp calendar_client.get_timestamp = lambda url,c,timeout=30: None base = {'origin_attestations':{'o1':{'origin_id':'o1','hash':'sha256:'+h.hex(),'hash_algo':'sha256','captured_at':'2026-07-09T00:00:00Z','short_token':'x'}}, 'core_ots_proofs':{'o1':{'origin_id':'o1','ots_proof':raw.hex(),'status':'pending','created_at':'2026-07-09T00:00:00Z'}}} st=copy.deepcopy(base); upgrade.upgrade_pending_proofs(st,FakeRPC({H_E3:mk(R,1)},H_E3+4),log=lambda m:None) check("D.2: k=5 nao promove", st['core_ots_proofs']['o1']['status']=='pending') st=copy.deepcopy(base); upgrade.upgrade_pending_proofs(st,FakeRPC({H_E3:mk(R,1)},H_E3+5),log=lambda m:None) check("D.2: k=6 promove com altura e block_time", st['core_ots_proofs']['o1']['status']=='anchored' and st['core_ots_proofs']['o1']['bitcoin_block_height']==H_E3) st=copy.deepcopy(base); upgrade.upgrade_pending_proofs(st,FakeRPC({},None),log=lambda m:None) check("D.2: no indisponivel fica pendente (nunca failed)", st['core_ots_proofs']['o1']['status']=='pending') calendar_client.get_timestamp = calendar_client_get else: print("aviso: vector1.ots ausente; testes VERIFY/D.2 saltados (extrai-o do draft com o proprio tests)") print() print("RESULTADO:", "TUDO PASSOU" if not FALHAS else f"{len(FALHAS)} FALHAS: {FALHAS}") sys.exit(1 if FALHAS else 0)