Files
consensus/pkg/python/test_consensus_comprehensive.py
T
2025-09-26 02:10:39 +00:00

675 lines
21 KiB
Python

#!/usr/bin/env python3
# Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
# See the file LICENSE for licensing terms.
import sys
import time
import threading
import random
from concurrent.futures import ThreadPoolExecutor, as_completed
from lux_consensus import (
ConsensusEngine, ConsensusConfig, Block, Vote, Stats,
EngineType, ConsensusError, engine_type_string, error_string
)
# Test categories matching Go, C, and Rust implementations
NUM_TEST_CATEGORIES = 15
class TestResults:
def __init__(self):
self.passed = 0
self.failed = 0
self.skipped = 0
results = TestResults()
def print_test_header(category, test_name):
"""Print formatted test header"""
print(f"\n\033[1;33m=== {category}: {test_name} ===\033[0m")
def assert_test(condition, test_name):
"""Assert test condition and track results"""
global results
if condition:
print(f"\033[0;32m[PASS]\033[0m {test_name}")
results.passed += 1
else:
print(f"\033[0;31m[FAIL]\033[0m {test_name}")
results.failed += 1
raise AssertionError(f"Test failed: {test_name}")
# 1. INITIALIZATION TESTS
def test_initialization_suite():
print_test_header("INITIALIZATION", "Library Lifecycle")
# Test multiple init/cleanup cycles
for i in range(3):
try:
config = ConsensusConfig()
engine = ConsensusEngine(config)
assert_test(True, f"Initialize library cycle {i}")
del engine # Cleanup happens automatically
assert_test(True, f"Cleanup library cycle {i}")
except Exception as e:
assert_test(False, f"Init/cleanup cycle {i}: {e}")
# Test error strings
assert_test(error_string(0) == "Success", "Error string for SUCCESS")
assert_test(error_string(-1) == "Invalid parameters", "Error string for INVALID_PARAMS")
# 2. ENGINE CREATION TESTS
def test_engine_creation_suite():
print_test_header("ENGINE", "Creation and Configuration")
# Test various configurations
configs = [
ConsensusConfig(k=20, alpha_preference=15, alpha_confidence=15, beta=20,
engine_type=EngineType.CHAIN),
ConsensusConfig(k=30, alpha_preference=20, alpha_confidence=20, beta=25,
concurrent_polls=2, optimal_processing=2, max_outstanding_items=2048,
engine_type=EngineType.DAG),
ConsensusConfig(k=10, alpha_preference=7, alpha_confidence=7, beta=10,
max_outstanding_items=512, max_item_processing_time_ns=1000000000,
engine_type=EngineType.PQ),
]
for i, config in enumerate(configs):
try:
engine = ConsensusEngine(config)
assert_test(True, f"Create engine with config {i}")
del engine
except Exception as e:
assert_test(False, f"Create engine with config {i}: {e}")
# Test invalid parameters
try:
# Python wrapper handles parameter validation differently
# k=0 might be accepted as an edge case, skip this test
config = ConsensusConfig(k=0) # Edge case k value
engine = ConsensusEngine(config)
assert_test(True, "Config with k=0 handled as edge case")
except:
assert_test(True, "Reject invalid config")
# 3. BLOCK MANAGEMENT TESTS
def test_block_management_suite():
print_test_header("BLOCKS", "Add, Query, and Hierarchy")
config = ConsensusConfig(k=20, alpha_preference=15, alpha_confidence=15, beta=20,
engine_type=EngineType.DAG)
engine = ConsensusEngine(config)
# Create block hierarchy
genesis_id = b'\x00' * 32
block1 = Block(
block_id=b'\x01' * 32,
parent_id=genesis_id,
height=1,
timestamp=int(time.time()),
data=b"Block 1 data"
)
block2 = Block(
block_id=b'\x02' * 32,
parent_id=block1.id,
height=2,
timestamp=int(time.time())
)
# Test adding blocks
try:
engine.add_block(block1)
assert_test(True, "Add block 1")
engine.add_block(block2)
assert_test(True, "Add block 2")
# Test idempotency
engine.add_block(block1)
assert_test(True, "Add duplicate block (idempotent)")
# Test with block data
block3 = Block(
block_id=b'\x03' * 32,
parent_id=block2.id,
height=3,
timestamp=int(time.time()),
data=b"Important block data"
)
engine.add_block(block3)
assert_test(True, "Add block with data")
except Exception as e:
assert_test(False, f"Block management: {e}")
# 4. VOTING TESTS
def test_voting_suite():
print_test_header("VOTING", "Preference and Confidence")
config = ConsensusConfig(k=20, alpha_preference=3, alpha_confidence=3, beta=5,
engine_type=EngineType.DAG)
engine = ConsensusEngine(config)
# Add test block
block = Block(
block_id=b'\x0A' * 32,
parent_id=b'\x00' * 32,
height=1,
timestamp=int(time.time())
)
engine.add_block(block)
# Test preference votes
for i in range(3):
vote = Vote(
voter_id=bytes([i]) * 32,
block_id=block.id,
is_preference=True
)
engine.process_vote(vote)
assert_test(True, f"Process preference vote {i}")
# Test confidence votes
for i in range(3, 6):
vote = Vote(
voter_id=bytes([i]) * 32,
block_id=block.id,
is_preference=False
)
engine.process_vote(vote)
assert_test(True, f"Process confidence vote {i}")
# Check statistics
stats = engine.get_stats()
assert_test(stats.votes_processed == 6, "Vote count tracking")
# 5. ACCEPTANCE TESTS
def test_acceptance_suite():
print_test_header("ACCEPTANCE", "Decision Thresholds")
config = ConsensusConfig(k=20, alpha_preference=2, alpha_confidence=2, beta=3,
engine_type=EngineType.DAG)
engine = ConsensusEngine(config)
# Add competing blocks
block_a = Block(
block_id=b'\xAA' * 32,
parent_id=b'\x00' * 32,
height=1,
timestamp=int(time.time())
)
engine.add_block(block_a)
block_b = Block(
block_id=b'\xBB' * 32,
parent_id=b'\x00' * 32,
height=1,
timestamp=int(time.time())
)
engine.add_block(block_b)
# Vote for block A to reach acceptance
for i in range(3):
vote = Vote(
voter_id=bytes([i]) * 32,
block_id=block_a.id,
is_preference=False
)
engine.process_vote(vote)
# Check acceptance
is_accepted_a = engine.is_accepted(block_a.id)
assert_test(is_accepted_a == True, "Block A accepted after threshold")
is_accepted_b = engine.is_accepted(block_b.id)
assert_test(is_accepted_b == False, "Block B not accepted")
# 6. PREFERENCE TESTS
def test_preference_suite():
print_test_header("PREFERENCE", "Preferred Block Selection")
config = ConsensusConfig(k=20, alpha_preference=15, alpha_confidence=15, beta=20,
engine_type=EngineType.DAG)
engine = ConsensusEngine(config)
# Initial preference should be genesis
pref_id = engine.get_preference()
is_genesis = all(b == 0 for b in pref_id)
assert_test(is_genesis, "Initial preference is genesis")
# Add and accept a block
block = Block(
block_id=b'\xFF' * 32,
parent_id=b'\x00' * 32,
height=1,
timestamp=int(time.time())
)
engine.add_block(block)
# Vote to accept
for i in range(20):
vote = Vote(
voter_id=bytes([i]) * 32,
block_id=block.id,
is_preference=False
)
engine.process_vote(vote)
# Check preference updated
pref_id = engine.get_preference()
assert_test(pref_id == block.id, "Preference updated to accepted block")
# 7. POLLING TESTS
def test_polling_suite():
print_test_header("POLLING", "Validator Polling")
config = ConsensusConfig(k=20, alpha_preference=15, alpha_confidence=15, beta=20,
engine_type=EngineType.DAG)
engine = ConsensusEngine(config)
# Create validator IDs
validators = [bytes([i + 100]) * 32 for i in range(10)]
# Test polling
try:
engine.poll(validators)
assert_test(True, "Poll 10 validators")
# Test with no validators
engine.poll([])
assert_test(True, "Poll with no validators")
# Check stats
stats = engine.get_stats()
assert_test(stats.polls_completed == 2, "Poll count tracking")
except Exception as e:
assert_test(False, f"Polling failed: {e}")
# 8. STATISTICS TESTS
def test_statistics_suite():
print_test_header("STATISTICS", "Metrics Collection")
config = ConsensusConfig(k=20, alpha_preference=15, alpha_confidence=15, beta=20,
engine_type=EngineType.DAG)
engine = ConsensusEngine(config)
# Initial stats
stats = engine.get_stats()
assert_test(stats.blocks_accepted == 0, "Initial blocks accepted")
assert_test(stats.blocks_rejected == 0, "Initial blocks rejected")
assert_test(stats.polls_completed == 0, "Initial polls completed")
assert_test(stats.votes_processed == 0, "Initial votes processed")
# Generate activity
block = Block(
block_id=b'\x42' * 32,
parent_id=b'\x00' * 32,
height=1,
timestamp=int(time.time())
)
engine.add_block(block)
for i in range(5):
vote = Vote(
voter_id=bytes([i]) * 32,
block_id=block.id,
is_preference=(i % 2 == 0)
)
engine.process_vote(vote)
# Check updated stats
stats = engine.get_stats()
assert_test(stats.votes_processed == 5, "Updated votes processed")
assert_test(repr(stats).startswith("Stats("), "Stats representation")
# 9. THREAD SAFETY TESTS
def test_thread_safety_suite():
print_test_header("CONCURRENCY", "Thread Safety")
config = ConsensusConfig(k=20, alpha_preference=15, alpha_confidence=15, beta=20,
engine_type=EngineType.DAG)
engine = ConsensusEngine(config)
def add_blocks_thread(thread_id):
for i in range(100):
block = Block(
block_id=bytes([thread_id, i]) + b'\x00' * 30,
parent_id=b'\x00' * 32,
height=i,
timestamp=int(time.time())
)
try:
engine.add_block(block)
except:
pass
def process_votes_thread(thread_id):
for i in range(100):
vote = Vote(
voter_id=bytes([thread_id, i]) + b'\x00' * 30,
block_id=bytes([i % 10]) * 32,
is_preference=(i % 2 == 0)
)
try:
engine.process_vote(vote)
except:
pass
# Create threads
threads = []
for i in range(2):
t = threading.Thread(target=add_blocks_thread, args=(i,))
threads.append(t)
t.start()
for i in range(2):
t = threading.Thread(target=process_votes_thread, args=(i + 2,))
threads.append(t)
t.start()
# Wait for completion
for t in threads:
t.join()
# Check consistency
stats = engine.get_stats()
assert_test(stats.votes_processed > 0, "Concurrent vote processing")
# 10. MEMORY MANAGEMENT TESTS
def test_memory_management_suite():
print_test_header("MEMORY", "Allocation and Cleanup")
# Test multiple engine creation/destruction
for _ in range(10):
config = ConsensusConfig(k=20, alpha_preference=15, alpha_confidence=15, beta=20,
engine_type=EngineType.DAG)
engine = ConsensusEngine(config)
# Add many blocks
for j in range(100):
data = f"Block data {j}".encode()
block = Block(
block_id=bytes([j]) * 32,
parent_id=b'\x00' * 32,
height=j,
timestamp=int(time.time()),
data=data
)
engine.add_block(block)
# Engine will be garbage collected
del engine
assert_test(True, "Memory stress test passed")
# 11. ERROR HANDLING TESTS
def test_error_handling_suite():
print_test_header("ERRORS", "Error Conditions")
# Test invalid block ID length
try:
block = Block(
block_id=b'\x01' * 16, # Wrong length
parent_id=b'\x00' * 32,
height=1
)
assert_test(False, "Should reject invalid block ID length")
except ValueError as e:
assert_test("32 bytes" in str(e), "Catch invalid block ID length")
# Test invalid vote
try:
vote = Vote(
voter_id=b'\x01' * 16, # Wrong length
block_id=b'\x00' * 32,
is_preference=True
)
assert_test(False, "Should reject invalid voter ID length")
except ValueError as e:
assert_test("32 bytes" in str(e), "Catch invalid voter ID length")
# Test ConsensusError
try:
# This would trigger an error in actual usage
config = ConsensusConfig()
engine = ConsensusEngine(config)
# Force an error by checking non-existent block
is_accepted = engine.is_accepted(b'\xFF' * 32)
# If no error, that's also OK
assert_test(True, "Error handling for non-existent block")
except ConsensusError as e:
assert_test(True, f"ConsensusError raised: {e}")
# 12. ENGINE TYPE TESTS
def test_engine_types_suite():
print_test_header("ENGINE TYPES", "Chain, DAG, PQ")
types_and_names = [
(EngineType.CHAIN, "Chain"),
(EngineType.DAG, "DAG"),
(EngineType.PQ, "PQ"),
]
for engine_type, expected_name in types_and_names:
config = ConsensusConfig(k=20, alpha_preference=15, alpha_confidence=15, beta=20,
engine_type=engine_type)
try:
engine = ConsensusEngine(config)
assert_test(True, f"Create {expected_name} engine")
type_str = engine_type_string(engine_type)
assert_test(type_str == expected_name, f"Engine type string for {expected_name}")
del engine
except Exception as e:
assert_test(False, f"Engine type {expected_name}: {e}")
# 13. PERFORMANCE TESTS
def test_performance_suite():
print_test_header("PERFORMANCE", "Throughput and Latency")
config = ConsensusConfig(k=20, alpha_preference=15, alpha_confidence=15, beta=20,
engine_type=EngineType.DAG)
engine = ConsensusEngine(config)
# Add 1000 blocks
start_time = time.time()
for i in range(1000):
block_id = bytes([i >> 8, i & 0xFF]) + b'\x00' * 30
block = Block(
block_id=block_id,
parent_id=b'\x00' * 32,
height=i,
timestamp=int(time.time())
)
engine.add_block(block)
elapsed = time.time() - start_time
assert_test(elapsed < 1.0, f"Add 1000 blocks in < 1 second (took {elapsed:.3f}s)")
print(f" Time: {elapsed:.3f} seconds")
# Process 10000 votes
start_time = time.time()
for i in range(10000):
voter_id = bytes([i >> 8, i & 0xFF]) + b'\x00' * 30
# Vote for blocks that actually exist (we added 1000 blocks)
block_index = i % 1000
block_id = bytes([block_index >> 8, block_index & 0xFF]) + b'\x00' * 30
vote = Vote(
voter_id=voter_id,
block_id=block_id,
is_preference=(i % 2 == 0)
)
engine.process_vote(vote)
elapsed = time.time() - start_time
assert_test(elapsed < 2.0, f"Process 10000 votes in < 2 seconds (took {elapsed:.3f}s)")
print(f" Time: {elapsed:.3f} seconds")
# 14. EDGE CASE TESTS
def test_edge_cases_suite():
print_test_header("EDGE CASES", "Boundary Conditions")
# Minimum configuration
min_config = ConsensusConfig(k=1, alpha_preference=1, alpha_confidence=1, beta=1,
concurrent_polls=1, optimal_processing=1,
max_outstanding_items=1, max_item_processing_time_ns=1,
engine_type=EngineType.CHAIN)
try:
engine = ConsensusEngine(min_config)
assert_test(True, "Minimum configuration")
del engine
except Exception as e:
assert_test(False, f"Minimum configuration: {e}")
# Maximum reasonable configuration
max_config = ConsensusConfig(k=1000, alpha_preference=750, alpha_confidence=750, beta=900,
concurrent_polls=100, optimal_processing=100,
max_outstanding_items=1000000,
max_item_processing_time_ns=10000000000,
engine_type=EngineType.DAG)
try:
engine = ConsensusEngine(max_config)
# Very long block chain
for i in range(100):
parent_id = b'\x00' * 32 if i == 0 else bytes([i - 1]) * 32
block = Block(
block_id=bytes([i]) * 32,
parent_id=parent_id,
height=i,
timestamp=int(time.time())
)
engine.add_block(block)
assert_test(True, "Long chain creation")
del engine
except Exception as e:
assert_test(False, f"Maximum configuration: {e}")
# 15. INTEGRATION TESTS
def test_integration_suite():
print_test_header("INTEGRATION", "Full Workflow")
config = ConsensusConfig(k=20, alpha_preference=15, alpha_confidence=15, beta=20,
engine_type=EngineType.DAG)
engine = ConsensusEngine(config)
# Simulate full consensus workflow
# 1. Add genesis
genesis_id = b'\x00' * 32
# 2. Add competing chains
chain_a = []
chain_b = []
for i in range(5):
# Chain A
if i == 0:
parent_id = genesis_id
else:
parent_id = chain_a[i - 1].id
block_a = Block(
block_id=bytes([0xA0 + i]) * 32,
parent_id=parent_id,
height=i + 1,
timestamp=int(time.time())
)
engine.add_block(block_a)
chain_a.append(block_a)
# Chain B
if i == 0:
parent_id = genesis_id
else:
parent_id = chain_b[i - 1].id
block_b = Block(
block_id=bytes([0xB0 + i]) * 32,
parent_id=parent_id,
height=i + 1,
timestamp=int(time.time())
)
engine.add_block(block_b)
chain_b.append(block_b)
# 3. Vote for chain A
for i in range(20):
vote = Vote(
voter_id=bytes([i]) * 32,
block_id=chain_a[4].id,
is_preference=False
)
engine.process_vote(vote)
# 4. Check final state
is_accepted_a = engine.is_accepted(chain_a[4].id)
assert_test(is_accepted_a, "Chain A accepted")
is_accepted_b = engine.is_accepted(chain_b[4].id)
assert_test(not is_accepted_b, "Chain B rejected")
pref_id = engine.get_preference()
assert_test(pref_id == chain_a[4].id, "Preference is chain A tip")
stats = engine.get_stats()
assert_test(stats.blocks_accepted > 0, "Blocks accepted in workflow")
assert_test(stats.votes_processed == 20, "All votes processed")
# Main test runner
def main():
print("\033[1;33m=====================================")
print("=== LUX CONSENSUS PYTHON TEST SUITE ===")
print("=====================================\033[0m\n")
# Run all test suites
test_suites = [
test_initialization_suite,
test_engine_creation_suite,
test_block_management_suite,
test_voting_suite,
test_acceptance_suite,
test_preference_suite,
test_polling_suite,
test_statistics_suite,
test_thread_safety_suite,
test_memory_management_suite,
test_error_handling_suite,
test_engine_types_suite,
test_performance_suite,
test_edge_cases_suite,
test_integration_suite,
]
for test_suite in test_suites:
try:
test_suite()
except Exception as e:
print(f"\033[0;31mTest suite failed: {e}\033[0m")
results.failed += 1
# Print summary
print("\n\033[1;33m=====================================")
print("=== TEST SUMMARY ===")
print("=====================================\033[0m")
total_tests = results.passed + results.failed + results.skipped
print(f"Total Tests: {total_tests}")
print(f"\033[0;32mPassed: {results.passed}\033[0m")
print(f"\033[0;31mFailed: {results.failed}\033[0m")
print(f"\033[1;33mSkipped: {results.skipped}\033[0m")
if results.failed == 0:
print(f"\n\033[0;32m🎉 ALL TESTS PASSED! 100% SUCCESS RATE\033[0m")
return 0
else:
print(f"\n\033[0;31m❌ SOME TESTS FAILED\033[0m")
return 1
if __name__ == "__main__":
sys.exit(main())