- Add Prometheus metrics for marketplace API throughput and error rates with new dashboard panels - Implement confidential transaction models with encryption support and access control - Add key management system with registration, rotation, and audit logging - Create services and registry routers for service discovery and management - Integrate ZK proof generation for privacy-preserving receipts - Add metrics instru
626 lines
22 KiB
Python
626 lines
22 KiB
Python
"""
|
|
End-to-end tests for AITBC Wallet Daemon
|
|
"""
|
|
|
|
import pytest
|
|
import asyncio
|
|
import json
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
import requests
|
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
from cryptography.hazmat.primitives import serialization
|
|
|
|
from packages.py.aitbc_crypto import sign_receipt, verify_receipt
|
|
from packages.py.aitbc_sdk import AITBCClient
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestWalletDaemonE2E:
|
|
"""End-to-end tests for wallet daemon functionality"""
|
|
|
|
@pytest.fixture
|
|
def wallet_base_url(self):
|
|
"""Wallet daemon base URL"""
|
|
return "http://localhost:8002"
|
|
|
|
@pytest.fixture
|
|
def coordinator_base_url(self):
|
|
"""Coordinator API base URL"""
|
|
return "http://localhost:8001"
|
|
|
|
@pytest.fixture
|
|
def test_wallet_data(self, temp_directory):
|
|
"""Create test wallet data"""
|
|
wallet_path = Path(temp_directory) / "test_wallet.json"
|
|
wallet_data = {
|
|
"id": "test-wallet-123",
|
|
"name": "Test Wallet",
|
|
"created_at": datetime.utcnow().isoformat(),
|
|
"accounts": [
|
|
{
|
|
"address": "0x1234567890abcdef",
|
|
"public_key": "test-public-key",
|
|
"encrypted_private_key": "encrypted-key-here",
|
|
}
|
|
],
|
|
}
|
|
|
|
with open(wallet_path, "w") as f:
|
|
json.dump(wallet_data, f)
|
|
|
|
return wallet_path
|
|
|
|
def test_wallet_creation_flow(self, wallet_base_url, temp_directory):
|
|
"""Test complete wallet creation flow"""
|
|
# Step 1: Create new wallet
|
|
create_data = {
|
|
"name": "E2E Test Wallet",
|
|
"password": "test-password-123",
|
|
"keystore_path": str(temp_directory),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets", json=create_data)
|
|
assert response.status_code == 201
|
|
|
|
wallet = response.json()
|
|
assert wallet["name"] == "E2E Test Wallet"
|
|
assert "id" in wallet
|
|
assert "accounts" in wallet
|
|
assert len(wallet["accounts"]) == 1
|
|
|
|
account = wallet["accounts"][0]
|
|
assert "address" in account
|
|
assert "public_key" in account
|
|
assert "encrypted_private_key" not in account # Should not be exposed
|
|
|
|
# Step 2: List wallets
|
|
response = requests.get(f"{wallet_base_url}/v1/wallets")
|
|
assert response.status_code == 200
|
|
|
|
wallets = response.json()
|
|
assert any(w["id"] == wallet["id"] for w in wallets)
|
|
|
|
# Step 3: Get wallet details
|
|
response = requests.get(f"{wallet_base_url}/v1/wallets/{wallet['id']}")
|
|
assert response.status_code == 200
|
|
|
|
wallet_details = response.json()
|
|
assert wallet_details["id"] == wallet["id"]
|
|
assert len(wallet_details["accounts"]) == 1
|
|
|
|
def test_wallet_unlock_flow(self, wallet_base_url, test_wallet_data):
|
|
"""Test wallet unlock and session management"""
|
|
# Step 1: Unlock wallet
|
|
unlock_data = {
|
|
"password": "test-password-123",
|
|
"keystore_path": str(test_wallet_data),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
|
assert response.status_code == 200
|
|
|
|
unlock_result = response.json()
|
|
assert "session_token" in unlock_result
|
|
assert "expires_at" in unlock_result
|
|
|
|
session_token = unlock_result["session_token"]
|
|
|
|
# Step 2: Use session for signing
|
|
headers = {"Authorization": f"Bearer {session_token}"}
|
|
|
|
sign_data = {
|
|
"message": "Test message to sign",
|
|
"account_address": "0x1234567890abcdef",
|
|
}
|
|
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/sign",
|
|
json=sign_data,
|
|
headers=headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
signature = response.json()
|
|
assert "signature" in signature
|
|
assert "public_key" in signature
|
|
|
|
# Step 3: Lock wallet
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/wallets/lock",
|
|
headers=headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# Step 4: Verify session is invalid
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/sign",
|
|
json=sign_data,
|
|
headers=headers
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
def test_receipt_verification_flow(self, wallet_base_url, coordinator_base_url, signed_receipt):
|
|
"""Test receipt verification workflow"""
|
|
# Step 1: Submit receipt to wallet for verification
|
|
verify_data = {
|
|
"receipt": signed_receipt,
|
|
}
|
|
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/receipts/verify",
|
|
json=verify_data
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
verification = response.json()
|
|
assert "valid" in verification
|
|
assert verification["valid"] is True
|
|
assert "verifications" in verification
|
|
|
|
# Check verification details
|
|
verifications = verification["verifications"]
|
|
assert "miner_signature" in verifications
|
|
assert "coordinator_signature" in verifications
|
|
assert verifications["miner_signature"]["valid"] is True
|
|
assert verifications["coordinator_signature"]["valid"] is True
|
|
|
|
# Step 2: Get receipt history
|
|
response = requests.get(f"{wallet_base_url}/v1/receipts")
|
|
assert response.status_code == 200
|
|
|
|
receipts = response.json()
|
|
assert len(receipts) > 0
|
|
assert any(r["id"] == signed_receipt["id"] for r in receipts)
|
|
|
|
def test_cross_component_integration(self, wallet_base_url, coordinator_base_url):
|
|
"""Test integration between wallet and coordinator"""
|
|
# Step 1: Create job via coordinator
|
|
job_data = {
|
|
"job_type": "ai_inference",
|
|
"parameters": {
|
|
"model": "gpt-3.5-turbo",
|
|
"prompt": "Test prompt",
|
|
},
|
|
}
|
|
|
|
response = requests.post(
|
|
f"{coordinator_base_url}/v1/jobs",
|
|
json=job_data,
|
|
headers={"X-Tenant-ID": "test-tenant"}
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
job = response.json()
|
|
job_id = job["id"]
|
|
|
|
# Step 2: Mock job completion and receipt creation
|
|
# In real test, this would involve actual miner execution
|
|
receipt_data = {
|
|
"id": f"receipt-{job_id}",
|
|
"job_id": job_id,
|
|
"miner_id": "test-miner",
|
|
"coordinator_id": "test-coordinator",
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"result": {"output": "Test result"},
|
|
}
|
|
|
|
# Sign receipt
|
|
private_key = ed25519.Ed25519PrivateKey.generate()
|
|
receipt_json = json.dumps({k: v for k, v in receipt_data.items() if k != "signature"})
|
|
signature = private_key.sign(receipt_json.encode())
|
|
receipt_data["signature"] = signature.hex()
|
|
|
|
# Step 3: Submit receipt to coordinator
|
|
response = requests.post(
|
|
f"{coordinator_base_url}/v1/receipts",
|
|
json=receipt_data
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
# Step 4: Fetch and verify receipt via wallet
|
|
response = requests.get(
|
|
f"{wallet_base_url}/v1/receipts/{receipt_data['id']}"
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
fetched_receipt = response.json()
|
|
assert fetched_receipt["id"] == receipt_data["id"]
|
|
assert fetched_receipt["job_id"] == job_id
|
|
|
|
def test_error_handling_flows(self, wallet_base_url):
|
|
"""Test error handling in various scenarios"""
|
|
# Test invalid password
|
|
unlock_data = {
|
|
"password": "wrong-password",
|
|
"keystore_path": "/nonexistent/path",
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
|
assert response.status_code == 400
|
|
assert "error" in response.json()
|
|
|
|
# Test invalid session token
|
|
headers = {"Authorization": "Bearer invalid-token"}
|
|
|
|
sign_data = {
|
|
"message": "Test",
|
|
"account_address": "0x123",
|
|
}
|
|
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/sign",
|
|
json=sign_data,
|
|
headers=headers
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
# Test invalid receipt format
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/receipts/verify",
|
|
json={"receipt": {"invalid": "data"}}
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_concurrent_operations(self, wallet_base_url, test_wallet_data):
|
|
"""Test concurrent wallet operations"""
|
|
import threading
|
|
import queue
|
|
|
|
# Unlock wallet first
|
|
unlock_data = {
|
|
"password": "test-password-123",
|
|
"keystore_path": str(test_wallet_data),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
|
session_token = response.json()["session_token"]
|
|
headers = {"Authorization": f"Bearer {session_token}"}
|
|
|
|
# Concurrent signing operations
|
|
results = queue.Queue()
|
|
|
|
def sign_message(message_id):
|
|
sign_data = {
|
|
"message": f"Test message {message_id}",
|
|
"account_address": "0x1234567890abcdef",
|
|
}
|
|
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/sign",
|
|
json=sign_data,
|
|
headers=headers
|
|
)
|
|
results.put((message_id, response.status_code, response.json()))
|
|
|
|
# Start 10 concurrent signing operations
|
|
threads = []
|
|
for i in range(10):
|
|
thread = threading.Thread(target=sign_message, args=(i,))
|
|
threads.append(thread)
|
|
thread.start()
|
|
|
|
# Wait for all threads to complete
|
|
for thread in threads:
|
|
thread.join()
|
|
|
|
# Verify all operations succeeded
|
|
success_count = 0
|
|
while not results.empty():
|
|
msg_id, status, result = results.get()
|
|
assert status == 200, f"Message {msg_id} failed"
|
|
success_count += 1
|
|
|
|
assert success_count == 10
|
|
|
|
def test_performance_limits(self, wallet_base_url, test_wallet_data):
|
|
"""Test performance limits and rate limiting"""
|
|
# Unlock wallet
|
|
unlock_data = {
|
|
"password": "test-password-123",
|
|
"keystore_path": str(test_wallet_data),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
|
session_token = response.json()["session_token"]
|
|
headers = {"Authorization": f"Bearer {session_token}"}
|
|
|
|
# Test rapid signing requests
|
|
start_time = time.time()
|
|
success_count = 0
|
|
|
|
for i in range(100):
|
|
sign_data = {
|
|
"message": f"Performance test {i}",
|
|
"account_address": "0x1234567890abcdef",
|
|
}
|
|
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/sign",
|
|
json=sign_data,
|
|
headers=headers
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
success_count += 1
|
|
elif response.status_code == 429:
|
|
# Rate limited
|
|
break
|
|
|
|
elapsed_time = time.time() - start_time
|
|
|
|
# Should handle at least 50 requests per second
|
|
assert success_count > 50
|
|
assert success_count / elapsed_time > 50
|
|
|
|
def test_wallet_backup_and_restore(self, wallet_base_url, temp_directory):
|
|
"""Test wallet backup and restore functionality"""
|
|
# Step 1: Create wallet with multiple accounts
|
|
create_data = {
|
|
"name": "Backup Test Wallet",
|
|
"password": "backup-password-123",
|
|
"keystore_path": str(temp_directory),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets", json=create_data)
|
|
wallet = response.json()
|
|
|
|
# Add additional account
|
|
unlock_data = {
|
|
"password": "backup-password-123",
|
|
"keystore_path": str(temp_directory),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
|
session_token = response.json()["session_token"]
|
|
headers = {"Authorization": f"Bearer {session_token}"}
|
|
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/accounts",
|
|
headers=headers
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
# Step 2: Create backup
|
|
backup_path = Path(temp_directory) / "wallet_backup.json"
|
|
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/wallets/{wallet['id']}/backup",
|
|
json={"backup_path": str(backup_path)},
|
|
headers=headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# Verify backup exists
|
|
assert backup_path.exists()
|
|
|
|
# Step 3: Restore wallet to new location
|
|
restore_dir = Path(temp_directory) / "restored"
|
|
restore_dir.mkdir()
|
|
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/wallets/restore",
|
|
json={
|
|
"backup_path": str(backup_path),
|
|
"restore_path": str(restore_dir),
|
|
"new_password": "restored-password-456",
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
restored_wallet = response.json()
|
|
assert len(restored_wallet["accounts"]) == 2
|
|
|
|
# Step 4: Verify restored wallet works
|
|
unlock_data = {
|
|
"password": "restored-password-456",
|
|
"keystore_path": str(restore_dir),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestWalletSecurityE2E:
|
|
"""End-to-end security tests for wallet daemon"""
|
|
|
|
def test_session_security(self, wallet_base_url, test_wallet_data):
|
|
"""Test session token security"""
|
|
# Unlock wallet to get session
|
|
unlock_data = {
|
|
"password": "test-password-123",
|
|
"keystore_path": str(test_wallet_data),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
|
session_token = response.json()["session_token"]
|
|
|
|
# Test session expiration
|
|
# In real test, this would wait for actual expiration
|
|
# For now, test invalid token format
|
|
invalid_tokens = [
|
|
"",
|
|
"invalid",
|
|
"Bearer invalid",
|
|
"Bearer ",
|
|
"Bearer " + "A" * 1000, # Too long
|
|
]
|
|
|
|
for token in invalid_tokens:
|
|
headers = {"Authorization": token}
|
|
response = requests.get(f"{wallet_base_url}/v1/wallets", headers=headers)
|
|
assert response.status_code == 401
|
|
|
|
def test_input_validation(self, wallet_base_url):
|
|
"""Test input validation and sanitization"""
|
|
# Test malicious inputs
|
|
malicious_inputs = [
|
|
{"name": "<script>alert('xss')</script>"},
|
|
{"password": "../../etc/passwd"},
|
|
{"keystore_path": "/etc/shadow"},
|
|
{"message": "\x00\x01\x02\x03"},
|
|
{"account_address": "invalid-address"},
|
|
]
|
|
|
|
for malicious_input in malicious_inputs:
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/wallets",
|
|
json=malicious_input
|
|
)
|
|
# Should either reject or sanitize
|
|
assert response.status_code in [400, 422]
|
|
|
|
def test_rate_limiting(self, wallet_base_url):
|
|
"""Test rate limiting on sensitive operations"""
|
|
# Test unlock rate limiting
|
|
unlock_data = {
|
|
"password": "test",
|
|
"keystore_path": "/nonexistent",
|
|
}
|
|
|
|
# Send rapid requests
|
|
rate_limited = False
|
|
for i in range(100):
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
|
if response.status_code == 429:
|
|
rate_limited = True
|
|
break
|
|
|
|
assert rate_limited, "Rate limiting should be triggered"
|
|
|
|
def test_encryption_strength(self, wallet_base_url, temp_directory):
|
|
"""Test wallet encryption strength"""
|
|
# Create wallet with strong password
|
|
create_data = {
|
|
"name": "Security Test Wallet",
|
|
"password": "VeryStr0ngP@ssw0rd!2024#SpecialChars",
|
|
"keystore_path": str(temp_directory),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets", json=create_data)
|
|
assert response.status_code == 201
|
|
|
|
# Verify keystore file is encrypted
|
|
keystore_path = Path(temp_directory) / "security-test-wallet.json"
|
|
assert keystore_path.exists()
|
|
|
|
with open(keystore_path, "r") as f:
|
|
keystore_data = json.load(f)
|
|
|
|
# Check that private keys are encrypted
|
|
for account in keystore_data.get("accounts", []):
|
|
assert "encrypted_private_key" in account
|
|
encrypted_key = account["encrypted_private_key"]
|
|
# Should not contain plaintext key material
|
|
assert "BEGIN PRIVATE KEY" not in encrypted_key
|
|
assert "-----END" not in encrypted_key
|
|
|
|
|
|
@pytest.mark.e2e
|
|
@pytest.mark.slow
|
|
class TestWalletPerformanceE2E:
|
|
"""Performance tests for wallet daemon"""
|
|
|
|
def test_large_wallet_performance(self, wallet_base_url, temp_directory):
|
|
"""Test performance with large number of accounts"""
|
|
# Create wallet
|
|
create_data = {
|
|
"name": "Large Wallet Test",
|
|
"password": "test-password-123",
|
|
"keystore_path": str(temp_directory),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets", json=create_data)
|
|
wallet = response.json()
|
|
|
|
# Unlock wallet
|
|
unlock_data = {
|
|
"password": "test-password-123",
|
|
"keystore_path": str(temp_directory),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
|
session_token = response.json()["session_token"]
|
|
headers = {"Authorization": f"Bearer {session_token}"}
|
|
|
|
# Create 100 accounts
|
|
start_time = time.time()
|
|
|
|
for i in range(100):
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/accounts",
|
|
headers=headers
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
creation_time = time.time() - start_time
|
|
|
|
# Should create accounts quickly
|
|
assert creation_time < 10.0, f"Account creation too slow: {creation_time}s"
|
|
|
|
# Test listing performance
|
|
start_time = time.time()
|
|
|
|
response = requests.get(
|
|
f"{wallet_base_url}/v1/wallets/{wallet['id']}",
|
|
headers=headers
|
|
)
|
|
|
|
listing_time = time.time() - start_time
|
|
assert response.status_code == 200
|
|
|
|
wallet_data = response.json()
|
|
assert len(wallet_data["accounts"]) == 101
|
|
assert listing_time < 1.0, f"Wallet listing too slow: {listing_time}s"
|
|
|
|
def test_concurrent_wallet_operations(self, wallet_base_url, temp_directory):
|
|
"""Test concurrent operations on multiple wallets"""
|
|
import concurrent.futures
|
|
|
|
def create_and_use_wallet(wallet_id):
|
|
wallet_dir = Path(temp_directory) / f"wallet_{wallet_id}"
|
|
wallet_dir.mkdir()
|
|
|
|
# Create wallet
|
|
create_data = {
|
|
"name": f"Concurrent Wallet {wallet_id}",
|
|
"password": f"password-{wallet_id}",
|
|
"keystore_path": str(wallet_dir),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets", json=create_data)
|
|
assert response.status_code == 201
|
|
|
|
# Unlock and sign
|
|
unlock_data = {
|
|
"password": f"password-{wallet_id}",
|
|
"keystore_path": str(wallet_dir),
|
|
}
|
|
|
|
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
|
session_token = response.json()["session_token"]
|
|
headers = {"Authorization": f"Bearer {session_token}"}
|
|
|
|
sign_data = {
|
|
"message": f"Message from wallet {wallet_id}",
|
|
"account_address": "0x1234567890abcdef",
|
|
}
|
|
|
|
response = requests.post(
|
|
f"{wallet_base_url}/v1/sign",
|
|
json=sign_data,
|
|
headers=headers
|
|
)
|
|
|
|
return response.status_code == 200
|
|
|
|
# Run 20 concurrent wallet operations
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
|
|
futures = [executor.submit(create_and_use_wallet, i) for i in range(20)]
|
|
results = [future.result() for future in concurrent.futures.as_completed(futures)]
|
|
|
|
# All operations should succeed
|
|
assert all(results), "Some concurrent wallet operations failed"
|