diff --git a/scripts/utils/common.sh b/scripts/utils/common.sh new file mode 100644 index 00000000..b9204aa3 --- /dev/null +++ b/scripts/utils/common.sh @@ -0,0 +1,356 @@ +#!/bin/bash +# ============================================================================ +# AITBC Mesh Network Implementation - Shared Utilities +# ============================================================================ +# This file contains common functions used by all phase scripts +# Source this file in other scripts: source /opt/aitbc/scripts/utils/common.sh +# ============================================================================ + +# Configuration +AITBC_ROOT="${AITBC_ROOT:-/opt/aitbc}" +CONFIG_DIR="${AITBC_ROOT}/config" +LOG_DIR="${AITBC_ROOT}/logs" +BACKUP_DIR="${AITBC_ROOT}/backups" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# ============================================================================ +# Logging Functions +# ============================================================================ + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" + log_to_file "INFO" "$1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" + log_to_file "WARN" "$1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} "$1"" + log_to_file "ERROR" "$1" +} + +log_debug() { + if [[ "${DEBUG_MODE}" == "true" ]]; then + echo -e "${BLUE}[DEBUG]${NC} $1" + log_to_file "DEBUG" "$1" + fi +} + +log_to_file() { + local level="$1" + local message="$2" + local script_name=$(basename "$0") + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + local log_file="${LOG_DIR}/${script_name%.sh}.log" + + # Create log directory if it doesn't exist + mkdir -p "${LOG_DIR}" + + # Append to log file + echo "${timestamp} [${level}] ${message}" >> "${log_file}" +} + +# ============================================================================ +# Backup Functions +# ============================================================================ + +backup_directory() { + local source_dir="$1" + local backup_name="${2:-$(basename $source_dir)}" + local timestamp=$(date +%Y%m%d_%H%M%S) + local backup_path="${BACKUP_DIR}/${backup_name}_backup_${timestamp}" + + log_info "Creating backup of ${source_dir}..." + + if [[ -d "$source_dir" ]]; then + mkdir -p "${BACKUP_DIR}" + cp -r "$source_dir" "$backup_path" + log_info "Backup created: ${backup_path}" + echo "$backup_path" # Return backup path + return 0 + else + log_warn "Source directory does not exist: ${source_dir}" + return 1 + fi +} + +restore_backup() { + local backup_path="$1" + local target_dir="$2" + + log_info "Restoring backup from ${backup_path}..." + + if [[ -d "$backup_path" ]]; then + rm -rf "$target_dir" + cp -r "$backup_path" "$target_dir" + log_info "Restored backup to ${target_dir}" + return 0 + else + log_error "Backup path does not exist: ${backup_path}" + return 1 + fi +} + +# ============================================================================ +# Validation Functions +# ============================================================================ + +validate_directory() { + local dir="$1" + local create_if_missing="${2:-false}" + + if [[ ! -d "$dir" ]]; then + if [[ "$create_if_missing" == "true" ]]; then + mkdir -p "$dir" + log_info "Created directory: ${dir}" + return 0 + else + log_error "Directory does not exist: ${dir}" + return 1 + fi + fi + return 0 +} + +validate_file() { + local file="$1" + + if [[ ! -f "$file" ]]; then + log_error "File does not exist: ${file}" + return 1 + fi + return 0 +} + +validate_command() { + local cmd="$1" + + if ! command -v "$cmd" &> /dev/null; then + log_error "Required command not found: ${cmd}" + return 1 + fi + return 0 +} + +# ============================================================================ +# Configuration Functions +# ============================================================================ + +create_config_file() { + local config_name="$1" + local config_content="$2" + local config_path="${CONFIG_DIR}/${config_name}" + + # Create config directory if it doesn't exist + mkdir -p "${CONFIG_DIR}" + + # Write config file + echo "$config_content" > "$config_path" + log_info "Created configuration file: ${config_path}" +} + +load_config_file() { + local config_name="$1" + local config_path="${CONFIG_DIR}/${config_name}" + + if [[ -f "$config_path" ]]; then + cat "$config_path" + return 0 + else + log_error "Configuration file not found: ${config_path}" + return 1 + fi +} + +# ============================================================================ +# Progress Tracking +# ============================================================================ + +show_progress() { + local current="$1" + local total="$2" + local message="${3:-Progress}" + local percentage=$((current * 100 / total)) + local bar_length=50 + local filled=$((percentage * bar_length / 100)) + local empty=$((bar_length - filled)) + + # Create progress bar + local bar="" + for ((i=0; i/dev/null || true + log_info "Service ${service_name} stopped" +} + +restart_service() { + local service_name="$1" + + log_info "Restarting service: ${service_name}" + systemctl restart "${service_name}" + log_info "Service ${service_name} restarted" +} + +# ============================================================================ +# Error Handling +# ============================================================================ + +handle_error() { + local error_code="$1" + local error_message="$2" + + log_error "Error ${error_code}: ${error_message}" + + # Log stack trace if DEBUG_MODE is enabled + if [[ "${DEBUG_MODE}" == "true" ]]; then + log_debug "Stack trace:" + local i=0 + while caller $i; do + ((i++)) + done | while read line file; do + log_debug " at ${file}:${line}" + done + fi + + exit "$error_code" +} + +set_error_handling() { + # Enable error handling + set -e + set -u + set -o pipefail + + # Set error trap + trap 'handle_error $? "Command failed at line $LINENO"' ERR +} + +# ============================================================================ +# Phase Management +# ============================================================================ + +mark_phase_complete() { + local phase_name="$1" + local completion_file="${CONFIG_DIR}/.completed_phases" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + mkdir -p "${CONFIG_DIR}" + echo "${phase_name}:${timestamp}" >> "${completion_file}" + log_info "Marked phase as complete: ${phase_name}" +} + +check_phase_complete() { + local phase_name="$1" + local completion_file="${CONFIG_DIR}/.completed_phases" + + if [[ -f "$completion_file" ]]; then + if grep -q "^${phase_name}:" "$completion_file"; then + return 0 # Phase is complete + fi + fi + return 1 # Phase not complete +} + +get_completed_phases() { + local completion_file="${CONFIG_DIR}/.completed_phases" + + if [[ -f "$completion_file" ]]; then + cat "$completion_file" | cut -d: -f1 + fi +} + +# ============================================================================ +# Python Code Generation Helpers +# ============================================================================ + +create_python_module() { + local module_path="$1" + local module_content="$2" + + # Create directory structure + local module_dir=$(dirname "$module_path") + mkdir -p "$module_dir" + + # Write module + echo "$module_content" > "$module_path" + log_info "Created Python module: ${module_path}" +} + +validate_python_syntax() { + local python_file="$1" + + if python3 -m py_compile "$python_file" 2>/dev/null; then + log_info "Python syntax validated: ${python_file}" + return 0 + else + log_error "Python syntax error in: ${python_file}" + return 1 + fi +} + +# ============================================================================ +# Initialization +# ============================================================================ + +init_common() { + # Set error handling + set_error_handling + + # Create necessary directories + mkdir -p "${CONFIG_DIR}" + mkdir -p "${LOG_DIR}" + mkdir -p "${BACKUP_DIR}" + + log_info "Common utilities initialized" + log_info "AITBC Root: ${AITBC_ROOT}" +} + +# Initialize if this script is sourced +if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + init_common +fi diff --git a/scripts/utils/env_config.sh b/scripts/utils/env_config.sh new file mode 100644 index 00000000..c9c51ccc --- /dev/null +++ b/scripts/utils/env_config.sh @@ -0,0 +1,463 @@ +#!/bin/bash +# ============================================================================ +# Environment Configuration Utility for AITBC Scripts +# ============================================================================ +# Provides environment-based configuration management +# Source this file: source /opt/aitbc/scripts/utils/env_config.sh +# ============================================================================ + +# Set default values if not already set +AITBC_ROOT="${AITBC_ROOT:-/opt/aitbc}" +AITBC_ENV="${AITBC_ENV:-development}" +DEBUG_MODE="${DEBUG_MODE:-false}" +DRY_RUN="${DRY_RUN:-false}" + +# Directory paths +CONFIG_DIR="${AITBC_ROOT}/config" +LOG_DIR="${AITBC_ROOT}/logs" +BACKUP_DIR="${AITBC_ROOT}/backups" +SCRIPTS_DIR="${AITBC_ROOT}/scripts" +APPS_DIR="${AITBC_ROOT}/apps" +TESTS_DIR="${AITBC_ROOT}/tests" + +# Phase-specific directories +CONSENSUS_DIR="${APPS_DIR}/blockchain-node/src/aitbc_chain/consensus" +NETWORK_DIR="${APPS_DIR}/blockchain-node/src/aitbc_chain/network" +ECONOMICS_DIR="${APPS_DIR}/blockchain-node/src/aitbc_chain/economics" +CONTRACTS_DIR="${APPS_DIR}/blockchain-node/src/aitbc_chain/contracts" +AGENT_SERVICES_DIR="${APPS_DIR}/agent-services" + +# Default configuration values +DEFAULT_VALIDATOR_COUNT="${DEFAULT_VALIDATOR_COUNT:-5}" +DEFAULT_BLOCK_TIME="${DEFAULT_BLOCK_TIME:-30}" +DEFAULT_MIN_STAKE="${DEFAULT_MIN_STAKE:-1000}" +DEFAULT_GAS_PRICE="${DEFAULT_GAS_PRICE:-0.001}" +DEFAULT_NETWORK_SIZE="${DEFAULT_NETWORK_SIZE:-50}" +DEFAULT_MAX_PEERS="${DEFAULT_MAX_PEERS:-50}" + +# Network configuration +BOOTSTRAP_NODES="${BOOTSTRAP_NODES:-10.1.223.93:8000,10.1.223.40:8000}" +DISCOVERY_INTERVAL="${DISCOVERY_INTERVAL:-30}" +HEARTBEAT_INTERVAL="${HEARTBEAT_INTERVAL:-60}" + +# Color codes +if [[ -t 1 ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + NC='\033[0m' +else + RED='' + GREEN='' + YELLOW='' + BLUE='' + NC='' +fi + +# ============================================================================ +# Configuration Functions +# ============================================================================ + +load_env_file() { + """ + Load environment variables from .env file + Usage: load_env_file [env_file_path] + """ + local env_file="${1:-${AITBC_ROOT}/.env}" + + if [[ -f "$env_file" ]]; then + log_info "Loading environment from ${env_file}" + set -a + source "$env_file" + set +a + else + log_warn "Environment file not found: ${env_file}" + fi +} + +save_env_file() { + """ + Save current environment configuration to .env file + Usage: save_env_file [env_file_path] + """ + local env_file="${1:-${AITBC_ROOT}/.env}" + + log_info "Saving environment configuration to ${env_file}" + + cat > "$env_file" << EOF +# AITBC Environment Configuration +# Generated: $(date) +# Environment: ${AITBC_ENV} + +AITBC_ROOT=${AITBC_ROOT} +AITBC_ENV=${AITBC_ENV} +DEBUG_MODE=${DEBUG_MODE} +DRY_RUN=${DRY_RUN} + +# Default Configuration +DEFAULT_VALIDATOR_COUNT=${DEFAULT_VALIDATOR_COUNT} +DEFAULT_BLOCK_TIME=${DEFAULT_BLOCK_TIME} +DEFAULT_MIN_STAKE=${DEFAULT_MIN_STAKE} +DEFAULT_GAS_PRICE=${DEFAULT_GAS_PRICE} +DEFAULT_NETWORK_SIZE=${DEFAULT_NETWORK_SIZE} +DEFAULT_MAX_PEERS=${DEFAULT_MAX_PEERS} + +# Network Configuration +BOOTSTRAP_NODES=${BOOTSTRAP_NODES} +DISCOVERY_INTERVAL=${DISCOVERY_INTERVAL} +HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL} +EOF + + log_info "Environment configuration saved" +} + +get_config_value() { + """ + Get configuration value with fallback to default + Usage: get_config_value key [default_value] + """ + local key="$1" + local default_value="${2:-}" + local value + + # Try environment variable first + value="${!key}" + + # If not set, try config file + if [[ -z "$value" && -f "${CONFIG_DIR}/env_config.json" ]]; then + value=$(jq -r ".${key} // empty" "${CONFIG_DIR}/env_config.json" 2>/dev/null) + fi + + # Fallback to default + if [[ -z "$value" ]]; then + value="$default_value" + fi + + echo "$value" +} + +set_config_value() { + """ + Set configuration value in environment and config file + Usage: set_config_value key value + """ + local key="$1" + local value="$2" + + # Export to environment + export "${key}=${value}" + + # Update config file if it exists + if [[ -f "${CONFIG_DIR}/env_config.json" ]]; then + local temp_file=$(mktemp) + jq --arg key "$key" --arg value "$value" '. + {($key): $value}' \ + "${CONFIG_DIR}/env_config.json" > "$temp_file" + mv "$temp_file" "${CONFIG_DIR}/env_config.json" + else + # Create new config file + mkdir -p "$CONFIG_DIR" + echo "{\"${key}\": \"${value}\"}" > "${CONFIG_DIR}/env_config.json" + fi + + log_info "Configuration set: ${key}=${value}" +} + +# ============================================================================ +# Environment Detection +# ============================================================================ + +detect_environment() { + """ + Detect current environment based on hostname, user, or other factors + Usage: detect_environment + Returns: environment_name + """ + local hostname=$(hostname) + local user=$(whoami) + + # Check for production indicators + if [[ "$hostname" =~ prod|production ]] || [[ "$AITBC_ENV" == "production" ]]; then + echo "production" + # Check for staging indicators + elif [[ "$hostname" =~ staging|stage|test ]] || [[ "$AITBC_ENV" == "staging" ]]; then + echo "staging" + # Default to development + else + echo "development" + fi +} + +validate_environment() { + """ + Validate that current environment is supported + Usage: validate_environment + Returns: 0 if valid, 1 if invalid + """ + local valid_envs=("development" "staging" "production" "testing") + local current_env=$(detect_environment) + + for env in "${valid_envs[@]}"; do + if [[ "$current_env" == "$env" ]]; then + return 0 + fi + done + + log_error "Invalid environment: ${current_env}" + log_error "Valid environments: ${valid_envs[*]}" + return 1 +} + +# ============================================================================ +# Environment-Specific Configuration +# ============================================================================ + +load_environment_config() { + """ + Load environment-specific configuration + Usage: load_environment_config [environment] + """ + local env="${1:-$(detect_environment)}" + local config_file="${CONFIG_DIR}/env.${env}.json" + + log_info "Loading configuration for environment: ${env}" + + if [[ -f "$config_file" ]]; then + # Load JSON configuration + while IFS='=' read -r key value; do + key=$(echo "$key" | tr -d '"') + value=$(echo "$value" | tr -d '"') + export "${key}=${value}" + done < <(jq -r 'to_entries | .[] | "\(.key)=\(.value)"' "$config_file") + + log_info "Loaded environment configuration from ${config_file}" + else + log_warn "Environment config not found: ${config_file}" + log_info "Using default configuration" + fi +} + +create_environment_config() { + """ + Create environment-specific configuration file + Usage: create_environment_config environment + """ + local env="$1" + local config_file="${CONFIG_DIR}/env.${env}.json" + + mkdir -p "$CONFIG_DIR" + + case "$env" in + "development") + cat > "$config_file" << EOF +{ + "AITBC_ENV": "development", + "DEBUG_MODE": "true", + "DEFAULT_VALIDATOR_COUNT": "3", + "DEFAULT_BLOCK_TIME": "10", + "DEFAULT_NETWORK_SIZE": "10", + "LOG_LEVEL": "DEBUG" +} +EOF + ;; + "staging") + cat > "$config_file" << EOF +{ + "AITBC_ENV": "staging", + "DEBUG_MODE": "false", + "DEFAULT_VALIDATOR_COUNT": "5", + "DEFAULT_BLOCK_TIME": "30", + "DEFAULT_NETWORK_SIZE": "20", + "LOG_LEVEL": "INFO" +} +EOF + ;; + "production") + cat > "$config_file" << EOF +{ + "AITBC_ENV": "production", + "DEBUG_MODE": "false", + "DEFAULT_VALIDATOR_COUNT": "10", + "DEFAULT_BLOCK_TIME": "30", + "DEFAULT_NETWORK_SIZE": "50", + "LOG_LEVEL": "WARN" +} +EOF + ;; + *) + log_error "Unknown environment: ${env}" + return 1 + ;; + esac + + log_info "Created environment configuration: ${config_file}" +} + +# ============================================================================ +# Validation Functions +# ============================================================================ + +validate_required_vars() { + """ + Validate that required environment variables are set + Usage: validate_required_vars var1 var2 var3 ... + """ + local missing_vars=() + + for var in "$@"; do + if [[ -z "${!var}" ]]; then + missing_vars+=("$var") + fi + done + + if [[ ${#missing_vars[@]} -gt 0 ]]; then + log_error "Missing required environment variables:" + for var in "${missing_vars[@]}"; do + log_error " - ${var}" + done + return 1 + fi + + return 0 +} + +validate_paths() { + """ + Validate that required paths exist + Usage: validate_paths + """ + local required_paths=( + "$AITBC_ROOT" + "$CONFIG_DIR" + "$APPS_DIR" + "$SCRIPTS_DIR" + ) + + local missing_paths=() + + for path in "${required_paths[@]}"; do + if [[ ! -d "$path" ]]; then + missing_paths+=("$path") + fi + done + + if [[ ${#missing_paths[@]} -gt 0 ]]; then + log_error "Missing required directories:" + for path in "${missing_paths[@]}"; do + log_error " - ${path}" + done + return 1 + fi + + return 0 +} + +# ============================================================================ +# Logging Functions +# ============================================================================ + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_debug() { + if [[ "${DEBUG_MODE}" == "true" ]]; then + echo -e "${BLUE}[DEBUG]${NC} $1" + fi +} + +# ============================================================================ +# Utility Functions +# ============================================================================ + +print_environment() { + """ + Print current environment configuration + Usage: print_environment + """ + echo "" + echo "============================================" + echo "AITBC Environment Configuration" + echo "============================================" + echo "AITBC_ROOT: ${AITBC_ROOT}" + echo "AITBC_ENV: ${AITBC_ENV}" + echo "DEBUG_MODE: ${DEBUG_MODE}" + echo "DRY_RUN: ${DRY_RUN}" + echo "" + echo "Default Configuration:" + echo " Validator Count: ${DEFAULT_VALIDATOR_COUNT}" + echo " Block Time: ${DEFAULT_BLOCK_TIME}" + echo " Min Stake: ${DEFAULT_MIN_STAKE}" + echo " Gas Price: ${DEFAULT_GAS_PRICE}" + echo " Network Size: ${DEFAULT_NETWORK_SIZE}" + echo "" + echo "Network Configuration:" + echo " Bootstrap Nodes: ${BOOTSTRAP_NODES}" + echo " Discovery Interval: ${DISCOVERY_INTERVAL}" + echo " Heartbeat Interval: ${HEARTBEAT_INTERVAL}" + echo "============================================" + echo "" +} + +is_dry_run() { + """ + Check if running in dry-run mode + Usage: is_dry_run + Returns: 0 if dry-run, 1 otherwise + """ + [[ "${DRY_RUN}" == "true" ]] +} + +is_debug() { + """ + Check if running in debug mode + Usage: is_debug + Returns: 0 if debug, 1 otherwise + """ + [[ "${DEBUG_MODE}" == "true" ]] +} + +# ============================================================================ +# Initialization +# ============================================================================ + +init_env_config() { + """ + Initialize environment configuration + Usage: init_env_config + """ + # Load .env file if it exists + load_env_file + + # Detect and set environment + AITBC_ENV=$(detect_environment) + export AITBC_ENV + + # Load environment-specific configuration + load_environment_config "$AITBC_ENV" + + # Validate environment + validate_environment || exit 1 + + # Validate paths + validate_paths || exit 1 + + log_info "Environment configuration initialized" + log_info "Environment: ${AITBC_ENV}" + + # Print configuration if debug mode + is_debug && print_environment +} + +# Initialize if this script is sourced +if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + init_env_config +fi diff --git a/tests/conftest_optimized.py b/tests/conftest_optimized.py new file mode 100644 index 00000000..7378dc90 --- /dev/null +++ b/tests/conftest_optimized.py @@ -0,0 +1,523 @@ +""" +Optimized Pytest Configuration and Fixtures for AITBC Mesh Network Tests +Provides session-scoped fixtures for improved test performance +""" + +import pytest +import asyncio +import os +import sys +import json +import time +from unittest.mock import Mock, AsyncMock +from decimal import Decimal +from typing import Dict, List, Any + +# Add project paths +sys.path.insert(0, '/opt/aitbc/apps/blockchain-node/src') +sys.path.insert(0, '/opt/aitbc/apps/agent-services/agent-registry/src') +sys.path.insert(0, '/opt/aitbc/apps/agent-services/agent-coordinator/src') +sys.path.insert(0, '/opt/aitbc/apps/agent-services/agent-bridge/src') +sys.path.insert(0, '/opt/aitbc/apps/agent-services/agent-compliance/src') + +# Global test configuration +TEST_CONFIG = { + "network_timeout": 30.0, + "consensus_timeout": 10.0, + "transaction_timeout": 5.0, + "mock_mode": True, + "integration_mode": False, + "performance_mode": False, +} + +# Test data constants +TEST_ADDRESSES = { + "validator_1": "0x1111111111111111111111111111111111111111", + "validator_2": "0x2222222222222222222222222222222222222222", + "validator_3": "0x3333333333333333333333333333333333333333", + "validator_4": "0x4444444444444444444444444444444444444444", + "validator_5": "0x5555555555555555555555555555555555555555", + "client_1": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "client_2": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "agent_1": "0xcccccccccccccccccccccccccccccccccccccccccc", + "agent_2": "0xdddddddddddddddddddddddddddddddddddddddddd", +} + +TEST_KEYS = { + "private_key_1": "0x1111111111111111111111111111111111111111111111111111111111111111", + "private_key_2": "0x2222222222222222222222222222222222222222222222222222222222222222", + "public_key_1": "0x031111111111111111111111111111111111111111111111111111111111111111", + "public_key_2": "0x032222222222222222222222222222222222222222222222222222222222222222", +} + +# Test constants +MIN_STAKE_AMOUNT = 1000.0 +DEFAULT_GAS_PRICE = 0.001 +DEFAULT_BLOCK_TIME = 30 +NETWORK_SIZE = 50 +AGENT_COUNT = 100 + +# ============================================================================ +# Session-Scoped Fixtures (Created once per test session) +# ============================================================================ + +@pytest.fixture(scope="session") +def event_loop(): + """Create an instance of the default event loop for the test session.""" + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + +@pytest.fixture(scope="session") +def test_config(): + """Provide test configuration - session scoped for consistency""" + return TEST_CONFIG.copy() + +@pytest.fixture(scope="session") +def test_addresses(): + """Provide test addresses - session scoped for consistency""" + return TEST_ADDRESSES.copy() + +@pytest.fixture(scope="session") +def test_keys(): + """Provide test keys - session scoped for consistency""" + return TEST_KEYS.copy() + +# ============================================================================ +# Phase 1: Consensus Layer - Session Scoped Fixtures +# ============================================================================ + +@pytest.fixture(scope="session") +def consensus_instances(): + """ + Create shared consensus instances for all tests. + Session-scoped to avoid recreating for each test. + """ + try: + from aitbc_chain.consensus.multi_validator_poa import MultiValidatorPoA + from aitbc_chain.consensus.rotation import ValidatorRotation, DEFAULT_ROTATION_CONFIG + from aitbc_chain.consensus.pbft import PBFTConsensus + from aitbc_chain.consensus.slashing import SlashingManager + from aitbc_chain.consensus.keys import KeyManager + + poa = MultiValidatorPoA("test-chain") + + # Add default validators + default_validators = [ + ("0x1111111111111111111111111111111111111111", 1000.0), + ("0x2222222222222222222222222222222222222222", 1000.0), + ("0x3333333333333333333333333333333333333333", 1000.0), + ] + + for address, stake in default_validators: + poa.add_validator(address, stake) + + instances = { + 'poa': poa, + 'rotation': ValidatorRotation(poa, DEFAULT_ROTATION_CONFIG), + 'pbft': PBFTConsensus(poa), + 'slashing': SlashingManager(), + 'keys': KeyManager(), + } + + yield instances + + # Cleanup if needed + instances.clear() + + except ImportError: + pytest.skip("Consensus modules not available", allow_module_level=True) + +@pytest.fixture(scope="function") +def fresh_poa(consensus_instances): + """ + Provide a fresh PoA instance for each test. + Uses session-scoped base but creates fresh copy. + """ + from aitbc_chain.consensus.multi_validator_poa import MultiValidatorPoA + return MultiValidatorPoA("test-chain") + +# ============================================================================ +# Phase 2: Network Layer - Session Scoped Fixtures +# ============================================================================ + +@pytest.fixture(scope="session") +def network_instances(): + """ + Create shared network instances for all tests. + Session-scoped to avoid recreating for each test. + """ + try: + from aitbc_chain.network.discovery import P2PDiscovery + from aitbc_chain.network.health import PeerHealthMonitor + from aitbc_chain.network.peers import DynamicPeerManager + from aitbc_chain.network.topology import NetworkTopology + + discovery = P2PDiscovery("test-node", "127.0.0.1", 8000) + health = PeerHealthMonitor(check_interval=60) + peers = DynamicPeerManager(discovery, health) + topology = NetworkTopology(discovery, health) + + instances = { + 'discovery': discovery, + 'health': health, + 'peers': peers, + 'topology': topology, + } + + yield instances + + except ImportError: + pytest.skip("Network modules not available", allow_module_level=True) + +# ============================================================================ +# Phase 3: Economic Layer - Session Scoped Fixtures +# ============================================================================ + +@pytest.fixture(scope="session") +def economic_instances(): + """ + Create shared economic instances for all tests. + Session-scoped to avoid recreating for each test. + """ + try: + from aitbc_chain.economics.staking import StakingManager + from aitbc_chain.economics.rewards import RewardDistributor, RewardCalculator + from aitbc_chain.economics.gas import GasManager + + staking = StakingManager(min_stake_amount=MIN_STAKE_AMOUNT) + calculator = RewardCalculator(base_reward_rate=0.05) + rewards = RewardDistributor(staking, calculator) + gas = GasManager(base_gas_price=DEFAULT_GAS_PRICE) + + instances = { + 'staking': staking, + 'rewards': rewards, + 'calculator': calculator, + 'gas': gas, + } + + yield instances + + except ImportError: + pytest.skip("Economic modules not available", allow_module_level=True) + +# ============================================================================ +# Phase 4: Agent Network - Session Scoped Fixtures +# ============================================================================ + +@pytest.fixture(scope="session") +def agent_instances(): + """ + Create shared agent instances for all tests. + Session-scoped to avoid recreating for each test. + """ + try: + from agent_services.agent_registry.src.registration import AgentRegistry + from agent_services.agent_registry.src.matching import CapabilityMatcher + from agent_services.agent_coordinator.src.reputation import ReputationManager + + registry = AgentRegistry() + matcher = CapabilityMatcher(registry) + reputation = ReputationManager() + + instances = { + 'registry': registry, + 'matcher': matcher, + 'reputation': reputation, + } + + yield instances + + except ImportError: + pytest.skip("Agent modules not available", allow_module_level=True) + +# ============================================================================ +# Phase 5: Smart Contract - Session Scoped Fixtures +# ============================================================================ + +@pytest.fixture(scope="session") +def contract_instances(): + """ + Create shared contract instances for all tests. + Session-scoped to avoid recreating for each test. + """ + try: + from aitbc_chain.contracts.escrow import EscrowManager + from aitbc_chain.contracts.disputes import DisputeResolver + + escrow = EscrowManager() + disputes = DisputeResolver() + + instances = { + 'escrow': escrow, + 'disputes': disputes, + } + + yield instances + + except ImportError: + pytest.skip("Contract modules not available", allow_module_level=True) + +# ============================================================================ +# Mock Fixtures - Function Scoped (Fresh for each test) +# ============================================================================ + +@pytest.fixture +def mock_consensus(): + """Mock consensus layer components - fresh for each test""" + class MockConsensus: + def __init__(self): + self.validators = {} + self.current_proposer = None + self.block_height = 100 + self.round_robin_index = 0 + + def add_validator(self, address, stake): + self.validators[address] = Mock(address=address, stake=stake) + return True + + def select_proposer(self, round_number=None): + if not self.validators: + return None + validator_list = list(self.validators.keys()) + index = (round_number or self.round_robin_index) % len(validator_list) + self.round_robin_index = index + 1 + self.current_proposer = validator_list[index] + return self.current_proposer + + def validate_transaction(self, tx): + return True, "valid" + + def process_block(self, block): + return True, "processed" + + return MockConsensus() + +@pytest.fixture +def mock_network(): + """Mock network layer components - fresh for each test""" + class MockNetwork: + def __init__(self): + self.peers = {} + self.connected_peers = set() + self.message_handler = Mock() + + def add_peer(self, peer_id, address, port): + self.peers[peer_id] = Mock(peer_id=peer_id, address=address, port=port) + self.connected_peers.add(peer_id) + return True + + def remove_peer(self, peer_id): + self.connected_peers.discard(peer_id) + if peer_id in self.peers: + del self.peers[peer_id] + return True + + def send_message(self, recipient, message_type, payload): + return True, "sent", f"msg_{int(time.time())}" + + def broadcast_message(self, message_type, payload): + return True, "broadcasted" + + def get_peer_count(self): + return len(self.connected_peers) + + return MockNetwork() + +@pytest.fixture +def mock_economics(): + """Mock economic layer components - fresh for each test""" + class MockEconomics: + def __init__(self): + self.stakes = {} + self.rewards = {} + self.gas_prices = {} + + def stake_tokens(self, address, amount): + self.stakes[address] = self.stakes.get(address, 0) + amount + return True, "staked" + + def unstake_tokens(self, address, amount): + if address in self.stakes and self.stakes[address] >= amount: + self.stakes[address] -= amount + return True, "unstaked" + return False, "insufficient stake" + + def calculate_reward(self, address, block_height): + return Decimal('10.0') + + def get_gas_price(self): + return Decimal(DEFAULT_GAS_PRICE) + + return MockEconomics() + +# ============================================================================ +# Sample Data Fixtures +# ============================================================================ + +@pytest.fixture +def sample_transactions(): + """Sample transaction data for testing""" + return [ + { + "tx_id": "tx_001", + "type": "transfer", + "from": TEST_ADDRESSES["client_1"], + "to": TEST_ADDRESSES["agent_1"], + "amount": Decimal('100.0'), + "gas_limit": 21000, + "gas_price": DEFAULT_GAS_PRICE + }, + { + "tx_id": "tx_002", + "type": "stake", + "from": TEST_ADDRESSES["validator_1"], + "amount": Decimal('1000.0'), + "gas_limit": 50000, + "gas_price": DEFAULT_GAS_PRICE + }, + ] + +@pytest.fixture +def sample_agents(): + """Sample agent data for testing""" + return [ + { + "agent_id": "agent_001", + "agent_type": "AI_MODEL", + "capabilities": ["text_generation", "summarization"], + "cost_per_use": Decimal('0.001'), + "reputation": 0.9 + }, + { + "agent_id": "agent_002", + "agent_type": "DATA_PROVIDER", + "capabilities": ["data_analysis", "prediction"], + "cost_per_use": Decimal('0.002'), + "reputation": 0.85 + }, + ] + +# ============================================================================ +# Test Configuration Fixtures +# ============================================================================ + +@pytest.fixture +def test_network_config(): + """Test network configuration""" + return { + "bootstrap_nodes": ["10.1.223.93:8000", "10.1.223.40:8000"], + "discovery_interval": 30, + "max_peers": 50, + "heartbeat_interval": 60 + } + +@pytest.fixture +def test_consensus_config(): + """Test consensus configuration""" + return { + "min_validators": 3, + "max_validators": 100, + "block_time": DEFAULT_BLOCK_TIME, + "consensus_timeout": 10, + "slashing_threshold": 0.1 + } + +@pytest.fixture +def test_economics_config(): + """Test economics configuration""" + return { + "min_stake": MIN_STAKE_AMOUNT, + "reward_rate": 0.05, + "gas_price": DEFAULT_GAS_PRICE, + "escrow_fee": 0.025, + "dispute_timeout": 604800 + } + +# ============================================================================ +# Pytest Configuration Hooks +# ============================================================================ + +def pytest_configure(config): + """Pytest configuration hook - add custom markers""" + config.addinivalue_line("markers", "unit: mark test as a unit test") + config.addinivalue_line("markers", "integration: mark test as an integration test") + config.addinivalue_line("markers", "performance: mark test as a performance test") + config.addinivalue_line("markers", "security: mark test as a security test") + config.addinivalue_line("markers", "slow: mark test as slow running") + +def pytest_collection_modifyitems(config, items): + """Modify test collection - add markers based on test location""" + for item in items: + if "performance" in str(item.fspath): + item.add_marker(pytest.mark.performance) + elif "security" in str(item.fspath): + item.add_marker(pytest.mark.security) + elif "integration" in str(item.fspath): + item.add_marker(pytest.mark.integration) + else: + item.add_marker(pytest.mark.unit) + +def pytest_ignore_collect(path, config): + """Ignore certain files during test collection""" + if "__pycache__" in str(path): + return True + if path.name.endswith(".bak") or path.name.endswith("~"): + return True + return False + +# ============================================================================ +# Test Helper Functions +# ============================================================================ + +def create_test_validator(address, stake=1000.0): + """Create a test validator""" + return Mock( + address=address, + stake=stake, + public_key=f"0x03{address[2:]}", + last_seen=time.time(), + status="active" + ) + +def create_test_agent(agent_id, agent_type="AI_MODEL", reputation=1.0): + """Create a test agent""" + return Mock( + agent_id=agent_id, + agent_type=agent_type, + reputation=reputation, + capabilities=["test_capability"], + endpoint=f"http://localhost:8000/{agent_id}", + created_at=time.time() + ) + +def assert_performance_metric(actual, expected, tolerance=0.1, metric_name="metric"): + """Assert performance metric within tolerance""" + lower_bound = expected * (1 - tolerance) + upper_bound = expected * (1 + tolerance) + + assert lower_bound <= actual <= upper_bound, ( + f"{metric_name} {actual} not within tolerance of expected {expected} " + f"(range: {lower_bound} - {upper_bound})" + ) + +async def async_wait_for_condition(condition, timeout=10.0, interval=0.1, description="condition"): + """Wait for async condition to be true""" + start_time = time.time() + + while time.time() - start_time < timeout: + if condition(): + return True + + await asyncio.sleep(interval) + + raise AssertionError(f"Timeout waiting for {description}") + +# ============================================================================ +# Environment Setup +# ============================================================================ + +os.environ.setdefault('AITBC_TEST_MODE', 'true') +os.environ.setdefault('AITBC_MOCK_MODE', 'true') +os.environ.setdefault('AITBC_LOG_LEVEL', 'DEBUG') diff --git a/tests/cross_phase/test_critical_failures.py b/tests/cross_phase/test_critical_failures.py new file mode 100644 index 00000000..df81893c --- /dev/null +++ b/tests/cross_phase/test_critical_failures.py @@ -0,0 +1,576 @@ +""" +Critical Failure Scenario Tests for AITBC Mesh Network +Tests system behavior under critical failure conditions +""" + +import pytest +import asyncio +import time +import random +from unittest.mock import Mock, patch, AsyncMock +from decimal import Decimal + +# Import required modules +try: + from aitbc_chain.consensus.multi_validator_poa import MultiValidatorPoA + from aitbc_chain.network.discovery import P2PDiscovery + from aitbc_chain.economics.staking import StakingManager + from agent_services.agent_registry.src.registration import AgentRegistry + from aitbc_chain.contracts.escrow import EscrowManager +except ImportError: + pytest.skip("Required modules not available", allow_module_level=True) + + +class TestConsensusDuringNetworkPartition: + """Test consensus behavior during network partition""" + + @pytest.fixture + def partitioned_consensus(self): + """Setup consensus in partitioned network scenario""" + poa = MultiValidatorPoA("partition-test") + + # Add validators across 3 partitions + partition_a = ["0xa1", "0xa2"] + partition_b = ["0xb1", "0xb2", "0xb3"] + partition_c = ["0xc1", "0xc2", "0xc3"] + + all_validators = partition_a + partition_b + partition_c + for v in all_validators: + poa.add_validator(v, 1000.0) + poa.activate_validator(v) + + return { + 'poa': poa, + 'partition_a': partition_a, + 'partition_b': partition_b, + 'partition_c': partition_c, + 'all_validators': all_validators + } + + @pytest.mark.asyncio + async def test_consensus_pauses_during_partition(self, partitioned_consensus): + """Test that consensus pauses when network is partitioned""" + poa = partitioned_consensus['poa'] + + # Simulate network partition detected + poa.network_partitioned = True + + # Attempt to create block should fail or be delayed + with pytest.raises(Exception) as exc_info: + await poa.create_block_during_partition() + + assert "partition" in str(exc_info.value).lower() or "paused" in str(exc_info.value).lower() + + @pytest.mark.asyncio + async def test_consensus_resumes_after_partition_healing(self, partitioned_consensus): + """Test that consensus resumes after network heals""" + poa = partitioned_consensus['poa'] + + # Start with partition + poa.network_partitioned = True + + # Heal partition + poa.network_partitioned = False + poa.last_partition_healed = time.time() + + # Wait minimum time before resuming + await asyncio.sleep(0.1) + + # Consensus should be able to resume + assert poa.can_resume_consensus() is True + + def test_partition_tolerant_to_minority_partition(self, partitioned_consensus): + """Test that consensus continues if minority is partitioned""" + poa = partitioned_consensus['poa'] + partition_a = partitioned_consensus['partition_a'] + + # Mark minority partition as isolated + for v in partition_a: + poa.mark_validator_partitioned(v) + + # Majority should still be able to reach consensus + majority_size = len(partitioned_consensus['all_validators']) - len(partition_a) + assert majority_size >= poa.quorum_size(8) # 5 validators remain (quorum = 5) + + @pytest.mark.asyncio + async def test_validator_churn_during_partition(self, partitioned_consensus): + """Test validator joining/leaving during network partition""" + poa = partitioned_consensus['poa'] + + # Simulate partition + poa.network_partitioned = True + + # Attempt to add new validator during partition + result = poa.add_validator("0xnew", 1000.0) + + # Should be queued or delayed + assert result is True or result is False # Depending on implementation + + +class TestEconomicCalculationsDuringValidatorChurn: + """Test economic consistency during validator changes""" + + @pytest.fixture + def economic_system_with_churn(self): + """Setup economic system with active validators""" + staking = StakingManager(min_stake_amount=1000.0) + + # Register initial validators + initial_validators = [f"0x{i}" for i in range(5)] + for v in initial_validators: + staking.register_validator(v, 2000.0, 0.05) + + # Record initial stake amounts + initial_stakes = {v: staking.get_total_staked() for v in initial_validators} + + return { + 'staking': staking, + 'initial_validators': initial_validators, + 'initial_stakes': initial_stakes + } + + def test_reward_calculation_during_validator_join(self, economic_system_with_churn): + """Test reward calculation when validator joins mid-epoch""" + staking = economic_system_with_churn['staking'] + + # Record state before new validator + total_stake_before = staking.get_total_staked() + validator_count_before = staking.get_validator_count() + + # New validator joins + new_validator = "0xnew_validator" + staking.register_validator(new_validator, 1500.0, 0.04) + + # Verify total stake updated correctly + total_stake_after = staking.get_total_staked() + assert total_stake_after > total_stake_before + + # Verify reward calculation includes new validator correctly + rewards = staking.calculate_epoch_rewards() + assert new_validator in rewards + + def test_reward_calculation_during_validator_exit(self, economic_system_with_churn): + """Test reward calculation when validator exits mid-epoch""" + staking = economic_system_with_churn['staking'] + exiting_validator = economic_system_with_churn['initial_validators'][0] + + # Record state before exit + total_stake_before = staking.get_total_staked() + + # Validator exits + staking.initiate_validator_exit(exiting_validator) + + # Stake should still be counted until unstaking period ends + total_stake_during_exit = staking.get_total_staked() + assert total_stake_during_exit == total_stake_before + + # After unstaking period + staking.complete_validator_exit(exiting_validator) + total_stake_after = staking.get_total_staked() + assert total_stake_after < total_stake_before + + def test_slashing_during_reward_distribution(self, economic_system_with_churn): + """Test that slashed validator doesn't receive rewards""" + staking = economic_system_with_churn['staking'] + + # Select validator to slash + slashed_validator = economic_system_with_churn['initial_validators'][1] + + # Add rewards to all validators + for v in economic_system_with_churn['initial_validators']: + staking.add_pending_rewards(v, 100.0) + + # Slash one validator + staking.slash_validator(slashed_validator, 0.1, "Double signing") + + # Distribute rewards + staking.distribute_rewards() + + # Slashed validator should have reduced or no rewards + slashed_rewards = staking.get_validator_rewards(slashed_validator) + other_rewards = staking.get_validator_rewards( + economic_system_with_churn['initial_validators'][2] + ) + + assert slashed_rewards < other_rewards + + @pytest.mark.asyncio + async def test_concurrent_stake_unstake_operations(self, economic_system_with_churn): + """Test concurrent staking operations don't corrupt state""" + staking = economic_system_with_churn['staking'] + + validator = economic_system_with_churn['initial_validators'][0] + + # Perform concurrent operations + async def stake_operation(): + staking.stake(validator, "0xdelegator1", 500.0) + + async def unstake_operation(): + await asyncio.sleep(0.01) # Slight delay + staking.unstake(validator, "0xdelegator2", 200.0) + + # Run concurrently + await asyncio.gather( + stake_operation(), + unstake_operation(), + stake_operation(), + return_exceptions=True + ) + + # Verify state is consistent + total_staked = staking.get_total_staked() + assert total_staked >= 0 # Should never be negative + + +class TestJobCompletionWithAgentFailure: + """Test job recovery when agent fails mid-execution""" + + @pytest.fixture + def job_with_escrow(self): + """Setup job with escrow contract""" + escrow = EscrowManager() + + # Create contract + success, _, contract_id = asyncio.run(escrow.create_contract( + job_id="job_001", + client_address="0xclient", + agent_address="0xagent", + amount=Decimal('100.0') + )) + + # Fund contract + asyncio.run(escrow.fund_contract(contract_id, "tx_hash")) + + return { + 'escrow': escrow, + 'contract_id': contract_id, + 'job_id': "job_001" + } + + @pytest.mark.asyncio + async def test_job_recovery_on_agent_failure(self, job_with_escrow): + """Test job recovery when agent fails""" + escrow = job_with_escrow['escrow'] + contract_id = job_with_escrow['contract_id'] + + # Start job + await escrow.start_job(contract_id) + + # Simulate agent failure + await escrow.report_agent_failure(contract_id, "0xagent", "Agent crashed") + + # Verify job can be reassigned + new_agent = "0xnew_agent" + success = await escrow.reassign_job(contract_id, new_agent) + + assert success is True + + # Verify contract state updated + contract = await escrow.get_contract_info(contract_id) + assert contract.agent_address == new_agent + + @pytest.mark.asyncio + async def test_escrow_refund_on_job_failure(self, job_with_escrow): + """Test client refund when job cannot be completed""" + escrow = job_with_escrow['escrow'] + contract_id = job_with_escrow['contract_id'] + + # Start job + await escrow.start_job(contract_id) + + # Mark job as failed + await escrow.fail_job(contract_id, "Technical failure") + + # Process refund + success, refund_amount = await escrow.process_refund(contract_id) + + assert success is True + assert refund_amount == Decimal('100.0') # Full refund + + # Verify contract state + contract = await escrow.get_contract_info(contract_id) + assert contract.state == "REFUNDED" + + @pytest.mark.asyncio + async def test_partial_completion_on_agent_failure(self, job_with_escrow): + """Test partial payment for completed work when agent fails""" + escrow = job_with_escrow['escrow'] + contract_id = job_with_escrow['contract_id'] + + # Setup milestones + milestones = [ + {'milestone_id': 'm1', 'amount': Decimal('30.0'), 'completed': True}, + {'milestone_id': 'm2', 'amount': Decimal('40.0'), 'completed': True}, + {'milestone_id': 'm3', 'amount': Decimal('30.0'), 'completed': False}, + ] + + for m in milestones: + await escrow.add_milestone(contract_id, m['milestone_id'], m['amount']) + if m['completed']: + await escrow.complete_milestone(contract_id, m['milestone_id']) + + # Agent fails before completing last milestone + await escrow.report_agent_failure(contract_id, "0xagent", "Agent failed") + + # Process partial payment + completed_amount = sum(m['amount'] for m in milestones if m['completed']) + agent_payment, client_refund = await escrow.process_partial_payment(contract_id) + + assert agent_payment == completed_amount + assert client_refund == Decimal('30.0') # Uncompleted milestone + + @pytest.mark.asyncio + async def test_multiple_agent_failures(self, job_with_escrow): + """Test job resilience through multiple agent failures""" + escrow = job_with_escrow['escrow'] + contract_id = job_with_escrow['contract_id'] + + # Start job + await escrow.start_job(contract_id) + + # Multiple agent failures + agents = ["0xagent1", "0xagent2", "0xagent3"] + + for i, agent in enumerate(agents): + if i > 0: + # Reassign to new agent + await escrow.reassign_job(contract_id, agent) + + # Simulate work then failure + await asyncio.sleep(0.01) + await escrow.report_agent_failure(contract_id, agent, f"Agent {i} failed") + + # Verify contract still valid + contract = await escrow.get_contract_info(contract_id) + assert contract.state in ["ACTIVE", "REASSIGNING", "DISPUTED"] + + +class TestSystemUnderHighLoad: + """Test system behavior under high load conditions""" + + @pytest.fixture + def loaded_system(self): + """Setup system under high load""" + return { + 'poa': MultiValidatorPoA("load-test"), + 'discovery': P2PDiscovery("load-node", "127.0.0.1", 8000), + 'staking': StakingManager(min_stake_amount=1000.0), + } + + @pytest.mark.asyncio + async def test_consensus_under_transaction_flood(self, loaded_system): + """Test consensus stability under transaction flood""" + poa = loaded_system['poa'] + + # Add validators + for i in range(10): + poa.add_validator(f"0x{i}", 1000.0) + poa.activate_validator(f"0x{i}") + + # Generate many concurrent transactions + transactions = [] + for i in range(1000): + tx = Mock( + tx_id=f"tx_{i}", + from_address="0xclient", + to_address="0xagent", + amount=Decimal('1.0') + ) + transactions.append(tx) + + # Process transactions under load + processed = 0 + failed = 0 + + async def process_tx(tx): + try: + await asyncio.wait_for( + poa.validate_transaction_async(tx), + timeout=1.0 + ) + return True + except asyncio.TimeoutError: + return False + except Exception: + return False + + # Process in batches + batch_size = 100 + for i in range(0, len(transactions), batch_size): + batch = transactions[i:i+batch_size] + results = await asyncio.gather(*[process_tx(tx) for tx in batch]) + processed += sum(results) + failed += len(batch) - sum(results) + + # Should process majority of transactions + assert processed > 800 # At least 80% success rate + + @pytest.mark.asyncio + async def test_network_under_peer_flood(self, loaded_system): + """Test network stability under peer connection flood""" + discovery = loaded_system['discovery'] + + # Simulate many peer connection attempts + peer_attempts = 500 + successful_connections = 0 + + for i in range(peer_attempts): + try: + result = await asyncio.wait_for( + discovery.attempt_peer_connection(f"127.0.0.1", 8001 + i), + timeout=0.1 + ) + if result: + successful_connections += 1 + except asyncio.TimeoutError: + pass + except Exception: + pass + + # Should not crash and should handle load gracefully + assert successful_connections >= 0 # Should not crash + + def test_memory_usage_under_load(self): + """Test memory usage remains bounded under high load""" + import psutil + import os + + process = psutil.Process(os.getpid()) + initial_memory = process.memory_info().rss / 1024 / 1024 # MB + + # Create large dataset + large_dataset = [] + for i in range(10000): + large_dataset.append({ + 'id': i, + 'data': 'x' * 1000, + 'timestamp': time.time(), + }) + + peak_memory = process.memory_info().rss / 1024 / 1024 + + # Clear dataset + del large_dataset + + final_memory = process.memory_info().rss / 1024 / 1024 + + # Memory should not grow unbounded + memory_increase = peak_memory - initial_memory + assert memory_increase < 500 # Less than 500MB increase + + +class TestByzantineFaultTolerance: + """Test Byzantine fault tolerance scenarios""" + + @pytest.fixture + def byzantine_setup(self): + """Setup with Byzantine validators""" + poa = MultiValidatorPoA("byzantine-test") + + # 7 validators: 2 honest, 2 faulty, 3 Byzantine + honest_validators = ["0xh1", "0xh2"] + faulty_validators = ["0xf1", "0xf2"] # Offline/crashed + byzantine_validators = ["0xb1", "0xb2", "0xb3"] # Malicious + + all_validators = honest_validators + faulty_validators + byzantine_validators + + for v in all_validators: + poa.add_validator(v, 1000.0) + poa.activate_validator(v) + + return { + 'poa': poa, + 'honest': honest_validators, + 'faulty': faulty_validators, + 'byzantine': byzantine_validators, + 'all': all_validators + } + + @pytest.mark.asyncio + async def test_consensus_with_byzantine_majority(self, byzantine_setup): + """Test consensus fails with Byzantine majority""" + poa = byzantine_setup['poa'] + + # With 3 Byzantine out of 7, they don't have majority + # But with 3 Byzantine + 2 faulty = 5, they could prevent consensus + + # Attempt to reach consensus + result = await poa.attempt_consensus( + block_hash="test_block", + round=1 + ) + + # Should fail due to insufficient honest validators + assert result is False or result is None + + def test_byzantine_behavior_detection(self, byzantine_setup): + """Test detection of Byzantine behavior""" + poa = byzantine_setup['poa'] + + # Simulate Byzantine behavior: inconsistent messages + byzantine_validator = byzantine_setup['byzantine'][0] + + # Send conflicting prepare messages + poa.record_prepare(byzantine_validator, "block_1", 1) + poa.record_prepare(byzantine_validator, "block_2", 1) # Conflict! + + # Should detect Byzantine behavior + is_byzantine = poa.detect_byzantine_behavior(byzantine_validator) + assert is_byzantine is True + + +class TestDataIntegrity: + """Test data integrity during failures""" + + def test_blockchain_state_consistency_after_crash(self): + """Test blockchain state remains consistent after crash recovery""" + poa = MultiValidatorPoA("integrity-test") + + # Add validators and create some blocks + validators = [f"0x{i}" for i in range(5)] + for v in validators: + poa.add_validator(v, 1000.0) + poa.activate_validator(v) + + # Record initial state hash + initial_state = poa.get_state_snapshot() + initial_hash = poa.calculate_state_hash(initial_state) + + # Simulate some operations + poa.create_block() + poa.add_transaction(Mock(tx_id="tx1")) + + # Simulate crash and recovery + recovered_state = poa.recover_state() + recovered_hash = poa.calculate_state_hash(recovered_state) + + # State should be consistent + assert recovered_hash == initial_hash or poa.validate_state_transition() + + def test_transaction_atomicity(self): + """Test transactions are atomic (all or nothing)""" + staking = StakingManager(min_stake_amount=1000.0) + + # Setup + staking.register_validator("0xvalidator", 2000.0, 0.05) + staking.stake("0xvalidator", "0xdelegator", 1500.0) + + initial_total = staking.get_total_staked() + + # Attempt complex transaction that should be atomic + try: + staking.execute_atomic_transaction([ + ('stake', '0xvalidator', '0xnew1', 500.0), + ('stake', '0xvalidator', '0xnew2', 500.0), + ('invalid_operation',) # This should fail + ]) + except Exception: + pass # Expected to fail + + # Verify state is unchanged (atomic rollback) + final_total = staking.get_total_staked() + assert final_total == initial_total + + +if __name__ == "__main__": + pytest.main([__file__, "-v", "--tb=short", "-x"]) diff --git a/tests/phase1/consensus/test_consensus.py b/tests/phase1/consensus/test_consensus.py new file mode 100644 index 00000000..e5409426 --- /dev/null +++ b/tests/phase1/consensus/test_consensus.py @@ -0,0 +1,351 @@ +""" +Phase 1: Consensus Layer Tests +Modularized consensus layer tests for AITBC Mesh Network +""" + +import pytest +import asyncio +import time +from unittest.mock import Mock +from decimal import Decimal + +# Import consensus components +try: + from aitbc_chain.consensus.multi_validator_poa import MultiValidatorPoA, ValidatorRole + from aitbc_chain.consensus.rotation import ValidatorRotation, RotationStrategy, DEFAULT_ROTATION_CONFIG + from aitbc_chain.consensus.pbft import PBFTConsensus, PBFTPhase, PBFTMessageType + from aitbc_chain.consensus.slashing import SlashingManager, SlashingCondition + from aitbc_chain.consensus.keys import KeyManager +except ImportError: + pytest.skip("Phase 1 consensus modules not available", allow_module_level=True) + + +class TestMultiValidatorPoA: + """Test Multi-Validator Proof of Authority Consensus""" + + @pytest.fixture + def poa(self): + """Create fresh PoA instance for each test""" + return MultiValidatorPoA("test-chain") + + def test_initialization(self, poa): + """Test multi-validator PoA initialization""" + assert poa.chain_id == "test-chain" + assert len(poa.validators) == 0 + assert poa.current_proposer_index == 0 + assert poa.round_robin_enabled is True + assert poa.consensus_timeout == 30 + + def test_add_validator(self, poa): + """Test adding validators""" + validator_address = "0x1234567890123456789012345678901234567890" + + success = poa.add_validator(validator_address, 1000.0) + assert success is True + assert validator_address in poa.validators + assert poa.validators[validator_address].stake == 1000.0 + assert poa.validators[validator_address].role == ValidatorRole.STANDBY + + def test_add_duplicate_validator(self, poa): + """Test adding duplicate validator fails""" + validator_address = "0x1234567890123456789012345678901234567890" + + poa.add_validator(validator_address, 1000.0) + success = poa.add_validator(validator_address, 2000.0) + assert success is False + + def test_remove_validator(self, poa): + """Test removing validator""" + validator_address = "0x1234567890123456789012345678901234567890" + poa.add_validator(validator_address, 1000.0) + + success = poa.remove_validator(validator_address) + assert success is True + assert validator_address not in poa.validators + + def test_select_proposer_round_robin(self, poa): + """Test round-robin proposer selection""" + validators = [ + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222", + "0x3333333333333333333333333333333333333333" + ] + + for validator in validators: + poa.add_validator(validator, 1000.0) + + proposers = [poa.select_proposer(i) for i in range(6)] + + assert all(p in validators for p in proposers[:3]) + assert proposers[0] == proposers[3] # Should cycle + + def test_activate_validator(self, poa): + """Test validator activation""" + validator_address = "0x1234567890123456789012345678901234567890" + poa.add_validator(validator_address, 1000.0) + + success = poa.activate_validator(validator_address) + assert success is True + assert poa.validators[validator_address].role == ValidatorRole.VALIDATOR + assert poa.validators[validator_address].is_active is True + + def test_set_proposer(self, poa): + """Test setting proposer role""" + validator_address = "0x1234567890123456789012345678901234567890" + poa.add_validator(validator_address, 1000.0) + poa.activate_validator(validator_address) + + success = poa.set_proposer(validator_address) + assert success is True + assert poa.validators[validator_address].role == ValidatorRole.PROPOSER + + +class TestValidatorRotation: + """Test Validator Rotation Mechanisms""" + + @pytest.fixture + def rotation(self): + """Create rotation instance with PoA""" + poa = MultiValidatorPoA("test-chain") + return ValidatorRotation(poa, DEFAULT_ROTATION_CONFIG) + + def test_rotation_strategies(self, rotation): + """Test different rotation strategies""" + # Add validators + for i in range(5): + rotation.poa.add_validator(f"0x{i}", 1000.0) + + # Test round-robin + rotation.config.strategy = RotationStrategy.ROUND_ROBIN + success = rotation.rotate_validators(100) + assert success is True + + # Test stake-weighted + rotation.config.strategy = RotationStrategy.STAKE_WEIGHTED + success = rotation.rotate_validators(101) + assert success is True + + # Test reputation-weighted + rotation.config.strategy = RotationStrategy.REPUTATION_WEIGHTED + success = rotation.rotate_validators(102) + assert success is True + + def test_rotation_interval(self, rotation): + """Test rotation respects intervals""" + assert rotation.config.min_blocks_between_rotations > 0 + + def test_rotation_with_no_validators(self, rotation): + """Test rotation with no validators""" + success = rotation.rotate_validators(100) + assert success is False + + +class TestPBFTConsensus: + """Test PBFT Byzantine Fault Tolerance""" + + @pytest.fixture + def pbft(self): + """Create PBFT instance""" + poa = MultiValidatorPoA("test-chain") + return PBFTConsensus(poa) + + @pytest.mark.asyncio + async def test_pre_prepare_phase(self, pbft): + """Test pre-prepare phase""" + success = await pbft.pre_prepare_phase( + "0xvalidator1", "block_hash_123", 1, + ["0xvalidator1", "0xvalidator2", "0xvalidator3"], + {"0xvalidator1": 0.9, "0xvalidator2": 0.8, "0xvalidator3": 0.85} + ) + assert success is True + + @pytest.mark.asyncio + async def test_prepare_phase(self, pbft): + """Test prepare phase""" + # First do pre-prepare + await pbft.pre_prepare_phase( + "0xvalidator1", "block_hash_123", 1, + ["0xvalidator1", "0xvalidator2", "0xvalidator3"], + {"0xvalidator1": 0.9, "0xvalidator2": 0.8, "0xvalidator3": 0.85} + ) + + # Then prepare + success = await pbft.prepare_phase("block_hash_123", 1) + assert success is True + + @pytest.mark.asyncio + async def test_commit_phase(self, pbft): + """Test commit phase""" + success = await pbft.commit_phase("block_hash_123", 1) + assert success is True + + def test_quorum_calculation(self, pbft): + """Test quorum calculation""" + assert pbft.quorum_size(4) == 3 # 2f+1 where f=1 + assert pbft.quorum_size(7) == 5 # 2f+1 where f=2 + assert pbft.quorum_size(10) == 7 # 2f+1 where f=3 + + def test_fault_tolerance_threshold(self, pbft): + """Test fault tolerance threshold""" + assert pbft.max_faulty_nodes(4) == 1 # floor((n-1)/3) + assert pbft.max_faulty_nodes(7) == 2 + assert pbft.max_faulty_nodes(10) == 3 + + +class TestSlashingManager: + """Test Slashing Condition Detection""" + + @pytest.fixture + def slashing(self): + """Create slashing manager""" + return SlashingManager() + + def test_double_sign_detection(self, slashing): + """Test double signing detection""" + validator_address = "0xvalidator1" + + event = slashing.detect_double_sign( + validator_address, "hash1", "hash2", 100 + ) + + assert event is not None + assert event.condition == SlashingCondition.DOUBLE_SIGN + assert event.validator_address == validator_address + + def test_downtime_detection(self, slashing): + """Test downtime detection""" + validator_address = "0xvalidator1" + + event = slashing.detect_excessive_downtime( + validator_address, missed_blocks=50, threshold=20 + ) + + assert event is not None + assert event.condition == SlashingCondition.EXCESSIVE_DOWNTIME + + def test_malicious_proposal_detection(self, slashing): + """Test malicious proposal detection""" + validator_address = "0xvalidator1" + + event = slashing.detect_malicious_proposal( + validator_address, "invalid_block_hash" + ) + + assert event is not None + assert event.condition == SlashingCondition.MALICIOUS_PROPOSAL + + def test_slashing_percentage(self, slashing): + """Test slashing percentage calculation""" + assert slashing.get_slashing_percentage(SlashingCondition.DOUBLE_SIGN) == 0.1 + assert slashing.get_slashing_percentage(SlashingCondition.EXCESSIVE_DOWNTIME) == 0.05 + assert slashing.get_slashing_percentage(SlashingCondition.MALICIOUS_PROPOSAL) == 0.1 + + +class TestKeyManager: + """Test Cryptographic Key Management""" + + @pytest.fixture + def key_manager(self): + """Create key manager""" + return KeyManager() + + def test_key_pair_generation(self, key_manager): + """Test key pair generation""" + address = "0x1234567890123456789012345678901234567890" + + key_pair = key_manager.generate_key_pair(address) + + assert key_pair.address == address + assert key_pair.private_key_pem is not None + assert key_pair.public_key_pem is not None + + def test_message_signing(self, key_manager): + """Test message signing""" + address = "0x1234567890123456789012345678901234567890" + key_manager.generate_key_pair(address) + + message = "test message" + signature = key_manager.sign_message(address, message) + + assert signature is not None + assert len(signature) > 0 + + def test_signature_verification(self, key_manager): + """Test signature verification""" + address = "0x1234567890123456789012345678901234567890" + key_manager.generate_key_pair(address) + + message = "test message" + signature = key_manager.sign_message(address, message) + + valid = key_manager.verify_signature(address, message, signature) + assert valid is True + + def test_invalid_signature(self, key_manager): + """Test invalid signature detection""" + address = "0x1234567890123456789012345678901234567890" + key_manager.generate_key_pair(address) + + message = "test message" + invalid_signature = "invalid_signature" + + valid = key_manager.verify_signature(address, message, invalid_signature) + assert valid is False + + def test_key_rotation(self, key_manager): + """Test key rotation""" + address = "0x1234567890123456789012345678901234567890" + + # Generate initial key + key_pair_1 = key_manager.generate_key_pair(address) + + # Rotate key + success = key_manager.rotate_key(address) + assert success is True + + # Get new key + key_pair_2 = key_manager.get_key_pair(address) + assert key_pair_2.public_key_pem != key_pair_1.public_key_pem + + +class TestConsensusIntegration: + """Test Integration Between Consensus Components""" + + def test_full_consensus_flow(self): + """Test complete consensus flow""" + # Setup components + poa = MultiValidatorPoA("test-chain") + pbft = PBFTConsensus(poa) + slashing = SlashingManager() + + # Add validators + for i in range(4): + poa.add_validator(f"0x{i}", 1000.0) + + # Test integration + assert poa is not None + assert pbft is not None + assert slashing is not None + + def test_rotation_with_slashing(self): + """Test rotation with slashed validator""" + poa = MultiValidatorPoA("test-chain") + rotation = ValidatorRotation(poa, DEFAULT_ROTATION_CONFIG) + slashing = SlashingManager() + + # Add validators + validators = [f"0x{i}" for i in range(4)] + for v in validators: + poa.add_validator(v, 1000.0) + + # Slash one validator + slashed_validator = validators[0] + slashing.apply_slash(slashed_validator, 0.1, "Test slash") + + # Rotation should skip slashed validator + success = rotation.rotate_validators(100) + assert success is True + + +if __name__ == "__main__": + pytest.main([__file__, "-v", "--tb=short"])