feat: add transaction hash search to blockchain explorer and cleanup settlement storage

Blockchain Explorer:
- Add transaction hash search support (64-char hex pattern validation)
- Fetch and display transaction details in modal (hash, type, from/to, amount, fee, block)
- Fix regex escape sequence in block height validation
- Update search placeholder text to mention both search types
- Add blank lines between function definitions for PEP 8 compliance

Settlement Storage:
- Add timedelta import for future
This commit is contained in:
oib
2026-02-17 14:34:12 +01:00
parent 31d3d70836
commit 421191ccaf
34 changed files with 2176 additions and 5660 deletions

View File

@@ -1,6 +1,8 @@
"""Ensure coordinator-api src is on sys.path for all tests in this directory."""
import sys
import os
import tempfile
from pathlib import Path
_src = str(Path(__file__).resolve().parent.parent / "src")
@@ -15,3 +17,9 @@ if _app_mod and hasattr(_app_mod, "__file__") and _app_mod.__file__ and _src not
if _src not in sys.path:
sys.path.insert(0, _src)
# Set up test environment
os.environ["TEST_MODE"] = "true"
project_root = Path(__file__).resolve().parent.parent.parent
os.environ["AUDIT_LOG_DIR"] = str(project_root / "logs" / "audit")
os.environ["TEST_DATABASE_URL"] = "sqlite:///:memory:"

View File

@@ -1,5 +1,5 @@
import pytest
from sqlmodel import Session, delete
from sqlmodel import Session, delete, text
from app.domain import Job, Miner
from app.models import JobCreate
@@ -14,7 +14,26 @@ def _init_db(tmp_path_factory):
from app.config import settings
settings.database_url = f"sqlite:///{db_file}"
# Initialize database and create tables
init_db()
# Ensure payment_id column exists (handle schema migration)
with session_scope() as sess:
try:
# Check if columns exist and add them if needed
result = sess.exec(text("PRAGMA table_info(job)"))
columns = [row[1] for row in result.fetchall()]
if 'payment_id' not in columns:
sess.exec(text("ALTER TABLE job ADD COLUMN payment_id TEXT"))
if 'payment_status' not in columns:
sess.exec(text("ALTER TABLE job ADD COLUMN payment_status TEXT"))
sess.commit()
except Exception as e:
print(f"Schema migration error: {e}")
sess.rollback()
yield

View File

@@ -9,19 +9,18 @@ from pathlib import Path
from app.services.zk_proofs import ZKProofService
from app.models import JobReceipt, Job, JobResult
from app.domain import ReceiptPayload
class TestZKProofService:
"""Test cases for ZK proof service"""
@pytest.fixture
def zk_service(self):
"""Create ZK proof service instance"""
with patch('app.services.zk_proofs.settings'):
with patch("app.services.zk_proofs.settings"):
service = ZKProofService()
return service
@pytest.fixture
def sample_job(self):
"""Create sample job for testing"""
@@ -31,9 +30,9 @@ class TestZKProofService:
payload={"type": "test"},
constraints={},
requested_at=None,
completed=True
completed=True,
)
@pytest.fixture
def sample_job_result(self):
"""Create sample job result"""
@@ -42,9 +41,9 @@ class TestZKProofService:
"result_hash": "0x1234567890abcdef",
"units": 100,
"unit_type": "gpu_seconds",
"metrics": {"execution_time": 5.0}
"metrics": {"execution_time": 5.0},
}
@pytest.fixture
def sample_receipt(self, sample_job):
"""Create sample receipt"""
@@ -59,171 +58,187 @@ class TestZKProofService:
price="0.1",
started_at=1640995200,
completed_at=1640995800,
metadata={}
metadata={},
)
return JobReceipt(
job_id=sample_job.id,
receipt_id=payload.receipt_id,
payload=payload.dict()
job_id=sample_job.id, receipt_id=payload.receipt_id, payload=payload.dict()
)
def test_service_initialization_with_files(self):
"""Test service initialization when circuit files exist"""
with patch('app.services.zk_proofs.Path') as mock_path:
with patch("app.services.zk_proofs.Path") as mock_path:
# Mock file existence
mock_path.return_value.exists.return_value = True
service = ZKProofService()
assert service.enabled is True
def test_service_initialization_without_files(self):
"""Test service initialization when circuit files are missing"""
with patch('app.services.zk_proofs.Path') as mock_path:
with patch("app.services.zk_proofs.Path") as mock_path:
# Mock file non-existence
mock_path.return_value.exists.return_value = False
service = ZKProofService()
assert service.enabled is False
@pytest.mark.asyncio
async def test_generate_proof_basic_privacy(self, zk_service, sample_receipt, sample_job_result):
async def test_generate_proof_basic_privacy(
self, zk_service, sample_receipt, sample_job_result
):
"""Test generating proof with basic privacy level"""
if not zk_service.enabled:
pytest.skip("ZK circuits not available")
# Mock subprocess calls
with patch('subprocess.run') as mock_run:
with patch("subprocess.run") as mock_run:
# Mock successful proof generation
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = json.dumps({
"proof": {"a": ["1", "2"], "b": [["1", "2"], ["1", "2"]], "c": ["1", "2"]},
"publicSignals": ["0x1234", "1000", "1640995800"]
})
mock_run.return_value.stdout = json.dumps(
{
"proof": {
"a": ["1", "2"],
"b": [["1", "2"], ["1", "2"]],
"c": ["1", "2"],
},
"publicSignals": ["0x1234", "1000", "1640995800"],
}
)
# Generate proof
proof = await zk_service.generate_receipt_proof(
receipt=sample_receipt,
job_result=sample_job_result,
privacy_level="basic"
privacy_level="basic",
)
assert proof is not None
assert "proof" in proof
assert "public_signals" in proof
assert proof["privacy_level"] == "basic"
assert "circuit_hash" in proof
@pytest.mark.asyncio
async def test_generate_proof_enhanced_privacy(self, zk_service, sample_receipt, sample_job_result):
async def test_generate_proof_enhanced_privacy(
self, zk_service, sample_receipt, sample_job_result
):
"""Test generating proof with enhanced privacy level"""
if not zk_service.enabled:
pytest.skip("ZK circuits not available")
with patch('subprocess.run') as mock_run:
with patch("subprocess.run") as mock_run:
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = json.dumps({
"proof": {"a": ["1", "2"], "b": [["1", "2"], ["1", "2"]], "c": ["1", "2"]},
"publicSignals": ["1000", "1640995800"]
})
mock_run.return_value.stdout = json.dumps(
{
"proof": {
"a": ["1", "2"],
"b": [["1", "2"], ["1", "2"]],
"c": ["1", "2"],
},
"publicSignals": ["1000", "1640995800"],
}
)
proof = await zk_service.generate_receipt_proof(
receipt=sample_receipt,
job_result=sample_job_result,
privacy_level="enhanced"
privacy_level="enhanced",
)
assert proof is not None
assert proof["privacy_level"] == "enhanced"
@pytest.mark.asyncio
async def test_generate_proof_service_disabled(self, zk_service, sample_receipt, sample_job_result):
async def test_generate_proof_service_disabled(
self, zk_service, sample_receipt, sample_job_result
):
"""Test proof generation when service is disabled"""
zk_service.enabled = False
proof = await zk_service.generate_receipt_proof(
receipt=sample_receipt,
job_result=sample_job_result,
privacy_level="basic"
receipt=sample_receipt, job_result=sample_job_result, privacy_level="basic"
)
assert proof is None
@pytest.mark.asyncio
async def test_generate_proof_invalid_privacy_level(self, zk_service, sample_receipt, sample_job_result):
async def test_generate_proof_invalid_privacy_level(
self, zk_service, sample_receipt, sample_job_result
):
"""Test proof generation with invalid privacy level"""
if not zk_service.enabled:
pytest.skip("ZK circuits not available")
with pytest.raises(ValueError, match="Unknown privacy level"):
await zk_service.generate_receipt_proof(
receipt=sample_receipt,
job_result=sample_job_result,
privacy_level="invalid"
privacy_level="invalid",
)
@pytest.mark.asyncio
async def test_verify_proof_success(self, zk_service):
"""Test successful proof verification"""
if not zk_service.enabled:
pytest.skip("ZK circuits not available")
with patch('subprocess.run') as mock_run, \
patch('builtins.open', mock_open(read_data='{"key": "value"}')):
with patch("subprocess.run") as mock_run, patch(
"builtins.open", mock_open(read_data='{"key": "value"}')
):
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = "true"
result = await zk_service.verify_proof(
proof={"a": ["1", "2"], "b": [["1", "2"], ["1", "2"]], "c": ["1", "2"]},
public_signals=["0x1234", "1000"]
public_signals=["0x1234", "1000"],
)
assert result is True
@pytest.mark.asyncio
async def test_verify_proof_failure(self, zk_service):
"""Test proof verification failure"""
if not zk_service.enabled:
pytest.skip("ZK circuits not available")
with patch('subprocess.run') as mock_run, \
patch('builtins.open', mock_open(read_data='{"key": "value"}')):
with patch("subprocess.run") as mock_run, patch(
"builtins.open", mock_open(read_data='{"key": "value"}')
):
mock_run.return_value.returncode = 1
mock_run.return_value.stderr = "Verification failed"
result = await zk_service.verify_proof(
proof={"a": ["1", "2"], "b": [["1", "2"], ["1", "2"]], "c": ["1", "2"]},
public_signals=["0x1234", "1000"]
public_signals=["0x1234", "1000"],
)
assert result is False
@pytest.mark.asyncio
async def test_verify_proof_service_disabled(self, zk_service):
"""Test proof verification when service is disabled"""
zk_service.enabled = False
result = await zk_service.verify_proof(
proof={"a": ["1", "2"], "b": [["1", "2"], ["1", "2"]], "c": ["1", "2"]},
public_signals=["0x1234", "1000"]
public_signals=["0x1234", "1000"],
)
assert result is False
def test_hash_receipt(self, zk_service, sample_receipt):
"""Test receipt hashing"""
receipt_hash = zk_service._hash_receipt(sample_receipt)
assert isinstance(receipt_hash, str)
assert len(receipt_hash) == 64 # SHA256 hex length
assert all(c in '0123456789abcdef' for c in receipt_hash)
assert all(c in "0123456789abcdef" for c in receipt_hash)
def test_serialize_receipt(self, zk_service, sample_receipt):
"""Test receipt serialization for circuit"""
serialized = zk_service._serialize_receipt(sample_receipt)
assert isinstance(serialized, list)
assert len(serialized) == 8
assert all(isinstance(x, str) for x in serialized)
@@ -231,19 +246,19 @@ class TestZKProofService:
class TestZKProofIntegration:
"""Integration tests for ZK proof system"""
@pytest.mark.asyncio
async def test_receipt_creation_with_zk_proof(self):
"""Test receipt creation with ZK proof generation"""
from app.services.receipts import ReceiptService
from sqlmodel import Session
# Create mock session
session = Mock(spec=Session)
# Create receipt service
receipt_service = ReceiptService(session)
# Create sample job
job = Job(
id="test-job-123",
@@ -251,43 +266,45 @@ class TestZKProofIntegration:
payload={"type": "test"},
constraints={},
requested_at=None,
completed=True
completed=True,
)
# Mock ZK proof service
with patch('app.services.receipts.zk_proof_service') as mock_zk:
with patch("app.services.receipts.zk_proof_service") as mock_zk:
mock_zk.is_enabled.return_value = True
mock_zk.generate_receipt_proof = AsyncMock(return_value={
"proof": {"a": ["1", "2"]},
"public_signals": ["0x1234"],
"privacy_level": "basic"
})
mock_zk.generate_receipt_proof = AsyncMock(
return_value={
"proof": {"a": ["1", "2"]},
"public_signals": ["0x1234"],
"privacy_level": "basic",
}
)
# Create receipt with privacy
receipt = await receipt_service.create_receipt(
job=job,
miner_id="miner-001",
job_result={"result": "test"},
result_metrics={"units": 100},
privacy_level="basic"
privacy_level="basic",
)
assert receipt is not None
assert "zk_proof" in receipt
assert receipt["privacy_level"] == "basic"
@pytest.mark.asyncio
async def test_settlement_with_zk_proof(self):
"""Test cross-chain settlement with ZK proof"""
from aitbc.settlement.hooks import SettlementHook
from aitbc.settlement.manager import BridgeManager
# Create mock bridge manager
bridge_manager = Mock(spec=BridgeManager)
# Create settlement hook
settlement_hook = SettlementHook(bridge_manager)
# Create sample job with ZK proof
job = Job(
id="test-job-123",
@@ -296,9 +313,9 @@ class TestZKProofIntegration:
constraints={},
requested_at=None,
completed=True,
target_chain=2
target_chain=2,
)
# Create receipt with ZK proof
receipt_payload = {
"version": "1.0",
@@ -306,24 +323,20 @@ class TestZKProofIntegration:
"job_id": job.id,
"provider": "miner-001",
"client": job.client_id,
"zk_proof": {
"proof": {"a": ["1", "2"]},
"public_signals": ["0x1234"]
}
"zk_proof": {"proof": {"a": ["1", "2"]}, "public_signals": ["0x1234"]},
}
job.receipt = JobReceipt(
job_id=job.id,
receipt_id=receipt_payload["receipt_id"],
payload=receipt_payload
payload=receipt_payload,
)
# Test settlement message creation
message = await settlement_hook._create_settlement_message(
job,
options={"use_zk_proof": True, "privacy_level": "basic"}
job, options={"use_zk_proof": True, "privacy_level": "basic"}
)
assert message.zk_proof is not None
assert message.privacy_level == "basic"
@@ -332,71 +345,70 @@ class TestZKProofIntegration:
def mock_open(read_data=""):
"""Mock open function for file operations"""
from unittest.mock import mock_open
return mock_open(read_data=read_data)
# Benchmark tests
class TestZKProofPerformance:
"""Performance benchmarks for ZK proof operations"""
@pytest.mark.asyncio
async def test_proof_generation_time(self):
"""Benchmark proof generation time"""
import time
if not Path("apps/zk-circuits/receipt.wasm").exists():
pytest.skip("ZK circuits not built")
service = ZKProofService()
if not service.enabled:
pytest.skip("ZK service not enabled")
# Create test data
receipt = JobReceipt(
job_id="benchmark-job",
receipt_id="benchmark-receipt",
payload={"test": "data"}
payload={"test": "data"},
)
job_result = {"result": "benchmark"}
# Measure proof generation time
start_time = time.time()
proof = await service.generate_receipt_proof(
receipt=receipt,
job_result=job_result,
privacy_level="basic"
receipt=receipt, job_result=job_result, privacy_level="basic"
)
end_time = time.time()
generation_time = end_time - start_time
assert proof is not None
assert generation_time < 30 # Should complete within 30 seconds
print(f"Proof generation time: {generation_time:.2f} seconds")
@pytest.mark.asyncio
async def test_proof_verification_time(self):
"""Benchmark proof verification time"""
import time
service = ZKProofService()
if not service.enabled:
pytest.skip("ZK service not enabled")
# Create test proof
proof = {"a": ["1", "2"], "b": [["1", "2"], ["1", "2"]], "c": ["1", "2"]}
public_signals = ["0x1234", "1000"]
# Measure verification time
start_time = time.time()
result = await service.verify_proof(proof, public_signals)
end_time = time.time()
verification_time = end_time - start_time
assert isinstance(result, bool)
assert verification_time < 1 # Should complete within 1 second
print(f"Proof verification time: {verification_time:.3f} seconds")