|
|
""" |
|
|
Módulo de integración con Polygon para publicación de digest de cumplimiento |
|
|
|
|
|
Este módulo publica hashes mensuales de autorizaciones en Polygon blockchain |
|
|
para garantizar trazabilidad y cumplimiento normativo público (AI Act, GDPR). |
|
|
""" |
|
|
|
|
|
import os |
|
|
import json |
|
|
import hashlib |
|
|
from typing import Dict, Any, List, Optional |
|
|
from datetime import datetime, timezone |
|
|
from dataclasses import dataclass |
|
|
import logging |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
@dataclass |
|
|
class DigestRecord: |
|
|
"""Registro de digest para publicación en Polygon""" |
|
|
period: str |
|
|
root_hash: str |
|
|
authorization_count: int |
|
|
timestamp: str |
|
|
publisher_address: str |
|
|
transaction_hash: Optional[str] = None |
|
|
block_number: Optional[int] = None |
|
|
gas_used: Optional[int] = None |
|
|
|
|
|
class PolygonDigestPublisher: |
|
|
"""Publicador de digest en Polygon blockchain""" |
|
|
|
|
|
def __init__(self): |
|
|
|
|
|
""" |
|
|
self.w3 = Web3(Web3.HTTPProvider(os.getenv("POLYGON_RPC_URL"))) |
|
|
self.private_key = os.getenv("POLYGON_WALLET_PRIVATE_KEY") |
|
|
self.account = self.w3.eth.account.from_key(self.private_key) |
|
|
self.chain_id = int(os.getenv("POLYGON_CHAIN_ID", "137")) # 137 mainnet, 80002 Amoy testnet |
|
|
|
|
|
self.contract_addr = os.getenv("DIGEST_CONTRACT_ADDR") |
|
|
self.contract_abi = json.loads(os.getenv("DIGEST_CONTRACT_ABI", "[]")) |
|
|
""" |
|
|
|
|
|
|
|
|
self.w3 = None |
|
|
self.account = None |
|
|
self.contract_addr = "0x0000000000000000000000000000000000000000" |
|
|
self.chain_id = 137 |
|
|
|
|
|
logger.info("PolygonDigestPublisher inicializado (modo simulado)") |
|
|
|
|
|
def _compute_monthly_digest(self, authorizations: List[Dict[str, Any]]) -> str: |
|
|
""" |
|
|
Calcula hash SHA-256 de todas las autorizaciones del mes |
|
|
|
|
|
Args: |
|
|
authorizations: Lista de registros de autorización del período |
|
|
|
|
|
Returns: |
|
|
Hash hexadecimal de 64 caracteres |
|
|
""" |
|
|
|
|
|
sorted_auths = sorted(authorizations, key=lambda x: x.get('timestamp', '')) |
|
|
|
|
|
|
|
|
digest_data = "" |
|
|
for auth in sorted_auths: |
|
|
|
|
|
relevant_data = { |
|
|
'user_email': auth.get('user_email', ''), |
|
|
'video_hash': auth.get('video_hash', ''), |
|
|
'timestamp': auth.get('timestamp', ''), |
|
|
'consent_accepted': auth.get('consent_accepted', False), |
|
|
'validation_status': auth.get('validation_status', ''), |
|
|
'document_id': auth.get('document_id', '') |
|
|
} |
|
|
|
|
|
|
|
|
auth_json = json.dumps(relevant_data, sort_keys=True, separators=(',', ':')) |
|
|
digest_data += auth_json + "|" |
|
|
|
|
|
|
|
|
digest_hash = hashlib.sha256(digest_data.encode('utf-8')).hexdigest() |
|
|
|
|
|
logger.info(f"Digest calculado: {len(authorizations)} autorizaciones → {digest_hash[:16]}...") |
|
|
return digest_hash |
|
|
|
|
|
def _get_period_from_timestamp(self, timestamp: str) -> str: |
|
|
""" |
|
|
Extrae período YYYY-MM del timestamp ISO |
|
|
|
|
|
Args: |
|
|
timestamp: Timestamp en formato ISO 8601 |
|
|
|
|
|
Returns: |
|
|
Período en formato YYYY-MM |
|
|
""" |
|
|
try: |
|
|
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) |
|
|
return dt.strftime("%Y-%m") |
|
|
except Exception as e: |
|
|
logger.error(f"Error parseando timestamp {timestamp}: {e}") |
|
|
return datetime.now(timezone.utc).strftime("%Y-%m") |
|
|
|
|
|
def publish_monthly_digest(self, authorizations: List[Dict[str, Any]]) -> Optional[DigestRecord]: |
|
|
""" |
|
|
Publica digest mensual en Polygon blockchain |
|
|
|
|
|
Args: |
|
|
authorizations: Lista de autorizaciones del período a publicar |
|
|
|
|
|
Returns: |
|
|
DigestRecord con resultado de la publicación |
|
|
""" |
|
|
if not authorizations: |
|
|
logger.warning("No hay autorizaciones para publicar") |
|
|
return None |
|
|
|
|
|
try: |
|
|
|
|
|
first_auth = authorizations[0] |
|
|
period = self._get_period_from_timestamp(first_auth.get('timestamp', '')) |
|
|
root_hash = self._compute_monthly_digest(authorizations) |
|
|
|
|
|
|
|
|
digest_record = DigestRecord( |
|
|
period=period, |
|
|
root_hash=root_hash, |
|
|
authorization_count=len(authorizations), |
|
|
timestamp=datetime.now(timezone.utc).isoformat(), |
|
|
publisher_address=self.account.address if self.account else "0x0000000000000000000000000000000000000000" |
|
|
) |
|
|
|
|
|
|
|
|
""" |
|
|
contract = self.w3.eth.contract( |
|
|
address=Web3.to_checksum_address(self.contract_addr), |
|
|
abi=self.contract_abi |
|
|
) |
|
|
|
|
|
nonce = self.w3.eth.get_transaction_count(self.account.address) |
|
|
|
|
|
tx = contract.functions.publish( |
|
|
Web3.to_bytes(hexstr=root_hash), |
|
|
period |
|
|
).build_transaction({ |
|
|
"from": self.account.address, |
|
|
"nonce": nonce, |
|
|
"gas": 120000, |
|
|
"maxFeePerGas": self.w3.to_wei('60', 'gwei'), |
|
|
"maxPriorityFeePerGas": self.w3.to_wei('2', 'gwei'), |
|
|
"chainId": self.chain_id |
|
|
}) |
|
|
|
|
|
signed = self.w3.eth.account.sign_transaction(tx, self.private_key) |
|
|
tx_hash = self.w3.eth.send_raw_transaction(signed.rawTransaction) |
|
|
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) |
|
|
|
|
|
# Actualizar registro con datos de la transacción |
|
|
digest_record.transaction_hash = receipt.transactionHash.hex() |
|
|
digest_record.block_number = receipt.blockNumber |
|
|
digest_record.gas_used = receipt.gasUsed |
|
|
""" |
|
|
|
|
|
|
|
|
simulated_tx_hash = f"0x{'0123456789abcdef' * 4}" |
|
|
digest_record.transaction_hash = simulated_tx_hash |
|
|
digest_record.block_number = 12345678 |
|
|
digest_record.gas_used = 87654 |
|
|
|
|
|
logger.info(f"Digest publicado simulado: {period} → {simulated_tx_hash}") |
|
|
|
|
|
return digest_record |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error publicando digest: {e}") |
|
|
return None |
|
|
|
|
|
def verify_digest_on_chain(self, period: str, expected_hash: str) -> bool: |
|
|
""" |
|
|
Verifica que el digest publicado en blockchain coincide con el hash esperado |
|
|
|
|
|
Args: |
|
|
period: Período YYYY-MM a verificar |
|
|
expected_hash: Hash esperado del digest |
|
|
|
|
|
Returns: |
|
|
True si coincide, False si no |
|
|
""" |
|
|
try: |
|
|
|
|
|
""" |
|
|
contract = self.w3.eth.contract( |
|
|
address=Web3.to_checksum_address(self.contract_addr), |
|
|
abi=self.contract_abi |
|
|
) |
|
|
|
|
|
on_chain_hash = contract.functions.digests(period).call() |
|
|
on_chain_hex = self.w3.to_hex(on_chain_hash) |
|
|
|
|
|
return on_chain_hex == expected_hash |
|
|
""" |
|
|
|
|
|
|
|
|
logger.info(f"Verificación simulada: {period} → {expected_hash[:16]}...") |
|
|
return True |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error verificando digest: {e}") |
|
|
return False |
|
|
|
|
|
def get_published_digests(self) -> List[Dict[str, Any]]: |
|
|
""" |
|
|
Obtiene lista de todos los digest publicados (simulado) |
|
|
|
|
|
Returns: |
|
|
Lista de digest publicados con metadata |
|
|
""" |
|
|
|
|
|
return [ |
|
|
{ |
|
|
"period": "2025-11", |
|
|
"transaction_hash": "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", |
|
|
"block_number": 12345678, |
|
|
"timestamp": "2025-11-03T14:30:00Z", |
|
|
"authorization_count": 42 |
|
|
} |
|
|
] |
|
|
|
|
|
|
|
|
digest_publisher = PolygonDigestPublisher() |
|
|
|