import hashlib, json, struct, sys def H(domain): return hashlib.blake2b(digest_size=32, key=domain) u16=lambda v: struct.pack('0: x.update(u64(mass)) else: x.update(u64(mass)) return x.hexdigest() def merkle_root(hashes): """kaspa_merkle::calc_merkle_root - MerkleBranchHash, relleno con ZERO_HASH""" if not hashes: return '00'*32 if len(hashes)==1: return hashes[0] n=1 while n1: nxt=[] for i in range(0,len(lvl),2): a,b=lvl[i],lvl[i+1] if a is None: nxt.append(None) else: m=H(b'MerkleBranchHash'); m.update(a); m.update(b if b is not None else ZERO) nxt.append(m.digest()) lvl=nxt return lvl[0].hex() # --------------------------------------------------------------------------- # Verificador autonomo de fecha cierta sobre Kaspa. # No necesita nodo, ni API, ni Proovik. Solo el bundle de evidencia. # --------------------------------------------------------------------------- from kaspa_pow import pow_hash, gen_matrix, heavy_hash, target_from_bits def tx_hash_full(tx): """tx::hash con TxEncodingFlags::FULL — lo que usa el arbol Merkle del bloque.""" x=H(b'TransactionHash'); ver=int(tx['version']) x.update(u16(ver)); x.update(u64(len(tx['inputs']))) for i in tx['inputs']: po=i['previousOutpoint'] x.update(b32(po['transactionId'])); x.update(u32(int(po.get('index',0) or 0))) var_bytes(x, bytes.fromhex(i.get('signatureScript') or '')) if ver<1: x.update(bytes([int(i.get('sigOpCount',0) or 0)])) x.update(u64(int(i.get('sequence',0) or 0))) if ver>=1: x.update(u16(int(i.get('computeBudget',0) or 0))) x.update(u64(len(tx['outputs']))) for o in tx['outputs']: x.update(u64(int(o['amount']))) spk=o['scriptPublicKey'] x.update(u16(int(spk.get('version',0) or 0))) var_bytes(x, bytes.fromhex(spk['scriptPublicKey'])) if ver>=1: # campo covenant (Toccata) cov=o.get('covenant') x.update(b'\x01' if cov else b'\x00') if cov: x.update(u16(int(cov['authorizingInput']))); x.update(b32(cov['covenantId'])) x.update(u64(int(tx.get('lockTime',0) or 0))) x.update(b32(tx['subnetworkId'])) x.update(u64(int(tx.get('gas',0) or 0))) var_bytes(x, bytes.fromhex(tx.get('payload') or '')) # ATENCION: es la STORAGE MASS (KIP-9) lo que entra en tx::hash, NO el campo # 'mass' que devuelve api.kaspa.org, que es la masa de computo. Debe venir # capturada en el bundle como 'storageMass'. Si es incorrecta, el paso 2 falla: # la comprobacion es autoverificable. mass=int(tx.get('storageMass', 0) or 0) if ver<1: if mass>0: x.update(u64(mass)) else: x.update(u64(mass)) return x.hexdigest() def verificar(bundle, hash_documento): """ bundle = {'header': , 'transactions': [...], # en el orden de verboseData.transactionIds 'block_hash': '...', 'txid': '...'} Devuelve (bool, [(eslabon, ok, detalle)]). """ r=[]; h=bundle['header'] tx=next((t for t in bundle['transactions'] if (t.get('verboseData') or {}).get('transactionId')==bundle['txid']), None) ok1 = tx is not None and (tx.get('payload') or '').lower()==hash_documento.lower() r.append(('1. El payload on-chain es el hash del documento', ok1, (tx or {}).get('payload','(tx no encontrada)'))) mr = merkle_root([tx_hash_full(t) for t in bundle['transactions']]) ok2 = mr == h['hashMerkleRoot'] r.append(('2. La transaccion esta en el arbol Merkle del bloque', ok2, mr)) bh = header_hash(h) ok3 = bh == bundle['block_hash'] r.append(('3. La cabecera produce el hash de bloque declarado', ok3, bh)) h0=dict(h); h0['nonce']=0; h0['timestamp']=0 pre=bytes.fromhex(header_hash(h0)) val=int.from_bytes(heavy_hash(gen_matrix(pre), pow_hash(pre,int(h['timestamp']),int(h['nonce']))),'little') tgt=target_from_bits(int(h['bits'])) ok4 = val<=tgt r.append(('4. La prueba de trabajo de esa cabecera es valida', ok4, f'pow={hex(val)[:20]}… target={hex(tgt)[:20]}…')) return all(x[1] for x in r), r def cabecera_truncada(bundle): """True si la cabecera trae un solo nivel de padres: no se puede recomputar el hash de bloque (eslabones 3-4). Ocurre cuando la prueba se capturo fuera de la ventana de ~40h y el indexador ya sirve la cabecera recortada.""" h = bundle.get('header', {}) p = h.get('parents') or h.get('parentsByLevel') or [] return len(p) <= 1 if __name__=='__main__': import sys b=json.load(open(sys.argv[1])); ok,det=verificar(b, sys.argv[2]) for nombre,v,d in det: print((' OK ' if v else ' FALLA') + ' ' + nombre + '\n ' + str(d)[:100]) # Tres estados, no dos: una prueba autentica capturada de forma degradada (cabecera # truncada) NO es lo mismo que una falsificacion. Solo se dice VALIDA con los 4 # eslabones; si fallan 3-4 pero por cabecera truncada, es NO CONCLUYENTE, no FALSA. e12 = det[0][1] and det[1][1] e34 = det[2][1] and det[3][1] if ok: print('\nRESULTADO: PRUEBA VALIDA (los 4 eslabones verificados)') elif e12 and not e34 and cabecera_truncada(b): print('\nRESULTADO: NO CONCLUYENTE — eslabones 1-2 verificados (el documento esta') print(' en esa transaccion y esa transaccion en ese bloque), pero 3-4 no') print(' son recomputables porque la cabecera viene truncada (un solo nivel') print(' de padres). No es una falsificacion: falta capturar la cabecera') print(' completa dentro de la ventana de ~40h.') else: print('\nRESULTADO: PRUEBA NO VALIDA (algun eslabon contradice los datos)')