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:
@@ -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:"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user