feat: implement AITBC mesh network deployment infrastructure
✅ Phase 0: Pre-implementation checklist completed - Environment configurations (dev/staging/production) - Directory structure setup (logs, backups, monitoring) - Virtual environment with dependencies ✅ Master deployment script created - Single command deployment with validation - Progress tracking and rollback capability - Health checks and deployment reporting ✅ Validation script created - Module import validation - Basic functionality testing - Configuration and script verification ✅ Implementation fixes - Fixed dataclass import in consensus keys - Fixed async function syntax in tests - Updated deployment script for virtual environment 🚀 Ready for deployment: ./scripts/deploy-mesh-network.sh dev
This commit is contained in:
166
apps/blockchain-node/tests/consensus/test_multi_validator_poa.py
Normal file
166
apps/blockchain-node/tests/consensus/test_multi_validator_poa.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Tests for Multi-Validator PoA Consensus
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aitbc_chain.consensus.multi_validator_poa import MultiValidatorPoA, ValidatorRole
|
||||
|
||||
class TestMultiValidatorPoA:
|
||||
"""Test cases for multi-validator PoA consensus"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup test environment"""
|
||||
self.consensus = MultiValidatorPoA("test-chain")
|
||||
|
||||
# Add test validators
|
||||
self.validator_addresses = [
|
||||
"0x1234567890123456789012345678901234567890",
|
||||
"0x2345678901234567890123456789012345678901",
|
||||
"0x3456789012345678901234567890123456789012",
|
||||
"0x4567890123456789012345678901234567890123",
|
||||
"0x5678901234567890123456789012345678901234"
|
||||
]
|
||||
|
||||
for address in self.validator_addresses:
|
||||
self.consensus.add_validator(address, 1000.0)
|
||||
|
||||
def test_add_validator(self):
|
||||
"""Test adding a new validator"""
|
||||
new_validator = "0x6789012345678901234567890123456789012345"
|
||||
|
||||
result = self.consensus.add_validator(new_validator, 1500.0)
|
||||
assert result is True
|
||||
assert new_validator in self.consensus.validators
|
||||
assert self.consensus.validators[new_validator].stake == 1500.0
|
||||
|
||||
def test_add_duplicate_validator(self):
|
||||
"""Test adding duplicate validator fails"""
|
||||
result = self.consensus.add_validator(self.validator_addresses[0], 2000.0)
|
||||
assert result is False
|
||||
|
||||
def test_remove_validator(self):
|
||||
"""Test removing a validator"""
|
||||
validator_to_remove = self.validator_addresses[0]
|
||||
|
||||
result = self.consensus.remove_validator(validator_to_remove)
|
||||
assert result is True
|
||||
assert not self.consensus.validators[validator_to_remove].is_active
|
||||
assert self.consensus.validators[validator_to_remove].role == ValidatorRole.STANDBY
|
||||
|
||||
def test_remove_nonexistent_validator(self):
|
||||
"""Test removing non-existent validator fails"""
|
||||
result = self.consensus.remove_validator("0xnonexistent")
|
||||
assert result is False
|
||||
|
||||
def test_select_proposer_round_robin(self):
|
||||
"""Test round-robin proposer selection"""
|
||||
# Set all validators as proposers
|
||||
for address in self.validator_addresses:
|
||||
self.consensus.validators[address].role = ValidatorRole.PROPOSER
|
||||
|
||||
# Test proposer selection for different heights
|
||||
proposer_0 = self.consensus.select_proposer(0)
|
||||
proposer_1 = self.consensus.select_proposer(1)
|
||||
proposer_2 = self.consensus.select_proposer(2)
|
||||
|
||||
assert proposer_0 in self.validator_addresses
|
||||
assert proposer_1 in self.validator_addresses
|
||||
assert proposer_2 in self.validator_addresses
|
||||
assert proposer_0 != proposer_1
|
||||
assert proposer_1 != proposer_2
|
||||
|
||||
def test_select_proposer_no_validators(self):
|
||||
"""Test proposer selection with no active validators"""
|
||||
# Deactivate all validators
|
||||
for address in self.validator_addresses:
|
||||
self.consensus.validators[address].is_active = False
|
||||
|
||||
proposer = self.consensus.select_proposer(0)
|
||||
assert proposer is None
|
||||
|
||||
def test_validate_block_valid_proposer(self):
|
||||
"""Test block validation with valid proposer"""
|
||||
from aitbc_chain.models import Block
|
||||
|
||||
# Set first validator as proposer
|
||||
proposer = self.validator_addresses[0]
|
||||
self.consensus.validators[proposer].role = ValidatorRole.PROPOSER
|
||||
|
||||
# Create mock block
|
||||
block = Mock(spec=Block)
|
||||
block.hash = "0xblockhash"
|
||||
block.height = 1
|
||||
|
||||
result = self.consensus.validate_block(block, proposer)
|
||||
assert result is True
|
||||
|
||||
def test_validate_block_invalid_proposer(self):
|
||||
"""Test block validation with invalid proposer"""
|
||||
from aitbc_chain.models import Block
|
||||
|
||||
# Create mock block
|
||||
block = Mock(spec=Block)
|
||||
block.hash = "0xblockhash"
|
||||
block.height = 1
|
||||
|
||||
# Try to validate with non-existent validator
|
||||
result = self.consensus.validate_block(block, "0xnonexistent")
|
||||
assert result is False
|
||||
|
||||
def test_get_consensus_participants(self):
|
||||
"""Test getting consensus participants"""
|
||||
# Set first 3 validators as active
|
||||
for i, address in enumerate(self.validator_addresses[:3]):
|
||||
self.consensus.validators[address].role = ValidatorRole.PROPOSER if i == 0 else ValidatorRole.VALIDATOR
|
||||
self.consensus.validators[address].is_active = True
|
||||
|
||||
# Set remaining validators as standby
|
||||
for address in self.validator_addresses[3:]:
|
||||
self.consensus.validators[address].role = ValidatorRole.STANDBY
|
||||
self.consensus.validators[address].is_active = False
|
||||
|
||||
participants = self.consensus.get_consensus_participants()
|
||||
assert len(participants) == 3
|
||||
assert self.validator_addresses[0] in participants
|
||||
assert self.validator_addresses[1] in participants
|
||||
assert self.validator_addresses[2] in participants
|
||||
assert self.validator_addresses[3] not in participants
|
||||
|
||||
def test_update_validator_reputation(self):
|
||||
"""Test updating validator reputation"""
|
||||
validator = self.validator_addresses[0]
|
||||
initial_reputation = self.consensus.validators[validator].reputation
|
||||
|
||||
# Increase reputation
|
||||
result = self.consensus.update_validator_reputation(validator, 0.1)
|
||||
assert result is True
|
||||
assert self.consensus.validators[validator].reputation == initial_reputation + 0.1
|
||||
|
||||
# Decrease reputation
|
||||
result = self.consensus.update_validator_reputation(validator, -0.2)
|
||||
assert result is True
|
||||
assert self.consensus.validators[validator].reputation == initial_reputation - 0.1
|
||||
|
||||
# Try to update non-existent validator
|
||||
result = self.consensus.update_validator_reputation("0xnonexistent", 0.1)
|
||||
assert result is False
|
||||
|
||||
def test_reputation_bounds(self):
|
||||
"""Test reputation stays within bounds [0.0, 1.0]"""
|
||||
validator = self.validator_addresses[0]
|
||||
|
||||
# Try to increase beyond 1.0
|
||||
result = self.consensus.update_validator_reputation(validator, 0.5)
|
||||
assert result is True
|
||||
assert self.consensus.validators[validator].reputation == 1.0
|
||||
|
||||
# Try to decrease below 0.0
|
||||
result = self.consensus.update_validator_reputation(validator, -1.5)
|
||||
assert result is True
|
||||
assert self.consensus.validators[validator].reputation == 0.0
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
402
apps/blockchain-node/tests/contracts/test_escrow.py
Normal file
402
apps/blockchain-node/tests/contracts/test_escrow.py
Normal file
@@ -0,0 +1,402 @@
|
||||
"""
|
||||
Tests for Escrow System
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import time
|
||||
from decimal import Decimal
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aitbc_chain.contracts.escrow import EscrowManager, EscrowState, DisputeReason
|
||||
|
||||
class TestEscrowManager:
|
||||
"""Test cases for escrow manager"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup test environment"""
|
||||
self.escrow_manager = EscrowManager()
|
||||
|
||||
def test_create_contract(self):
|
||||
"""Test escrow contract creation"""
|
||||
success, message, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="job_001",
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0')
|
||||
)
|
||||
)
|
||||
|
||||
assert success, f"Contract creation failed: {message}"
|
||||
assert contract_id is not None
|
||||
|
||||
# Check contract details
|
||||
contract = asyncio.run(self.escrow_manager.get_contract_info(contract_id))
|
||||
assert contract is not None
|
||||
assert contract.job_id == "job_001"
|
||||
assert contract.client_address == "0x1234567890123456789012345678901234567890"
|
||||
assert contract.agent_address == "0x2345678901234567890123456789012345678901"
|
||||
assert contract.amount > Decimal('100.0') # Includes platform fee
|
||||
assert contract.state == EscrowState.CREATED
|
||||
|
||||
def test_create_contract_invalid_inputs(self):
|
||||
"""Test contract creation with invalid inputs"""
|
||||
success, message, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="", # Empty job ID
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0')
|
||||
)
|
||||
)
|
||||
|
||||
assert not success
|
||||
assert contract_id is None
|
||||
assert "invalid" in message.lower()
|
||||
|
||||
def test_create_contract_with_milestones(self):
|
||||
"""Test contract creation with milestones"""
|
||||
milestones = [
|
||||
{
|
||||
'milestone_id': 'milestone_1',
|
||||
'description': 'Initial setup',
|
||||
'amount': Decimal('30.0')
|
||||
},
|
||||
{
|
||||
'milestone_id': 'milestone_2',
|
||||
'description': 'Main work',
|
||||
'amount': Decimal('50.0')
|
||||
},
|
||||
{
|
||||
'milestone_id': 'milestone_3',
|
||||
'description': 'Final delivery',
|
||||
'amount': Decimal('20.0')
|
||||
}
|
||||
]
|
||||
|
||||
success, message, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="job_002",
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0'),
|
||||
milestones=milestones
|
||||
)
|
||||
)
|
||||
|
||||
assert success
|
||||
assert contract_id is not None
|
||||
|
||||
# Check milestones
|
||||
contract = asyncio.run(self.escrow_manager.get_contract_info(contract_id))
|
||||
assert len(contract.milestones) == 3
|
||||
assert contract.milestones[0]['amount'] == Decimal('30.0')
|
||||
assert contract.milestones[1]['amount'] == Decimal('50.0')
|
||||
assert contract.milestones[2]['amount'] == Decimal('20.0')
|
||||
|
||||
def test_create_contract_invalid_milestones(self):
|
||||
"""Test contract creation with invalid milestones"""
|
||||
milestones = [
|
||||
{
|
||||
'milestone_id': 'milestone_1',
|
||||
'description': 'Setup',
|
||||
'amount': Decimal('30.0')
|
||||
},
|
||||
{
|
||||
'milestone_id': 'milestone_2',
|
||||
'description': 'Main work',
|
||||
'amount': Decimal('80.0') # Total exceeds contract amount
|
||||
}
|
||||
]
|
||||
|
||||
success, message, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="job_003",
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0'),
|
||||
milestones=milestones
|
||||
)
|
||||
)
|
||||
|
||||
assert not success
|
||||
assert "milestones" in message.lower()
|
||||
|
||||
def test_fund_contract(self):
|
||||
"""Test funding contract"""
|
||||
# Create contract first
|
||||
success, _, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="job_004",
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0')
|
||||
)
|
||||
)
|
||||
|
||||
assert success
|
||||
|
||||
# Fund contract
|
||||
success, message = asyncio.run(
|
||||
self.escrow_manager.fund_contract(contract_id, "tx_hash_001")
|
||||
)
|
||||
|
||||
assert success, f"Contract funding failed: {message}"
|
||||
|
||||
# Check state
|
||||
contract = asyncio.run(self.escrow_manager.get_contract_info(contract_id))
|
||||
assert contract.state == EscrowState.FUNDED
|
||||
|
||||
def test_fund_already_funded_contract(self):
|
||||
"""Test funding already funded contract"""
|
||||
# Create and fund contract
|
||||
success, _, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="job_005",
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0')
|
||||
)
|
||||
)
|
||||
|
||||
asyncio.run(self.escrow_manager.fund_contract(contract_id, "tx_hash_001"))
|
||||
|
||||
# Try to fund again
|
||||
success, message = asyncio.run(
|
||||
self.escrow_manager.fund_contract(contract_id, "tx_hash_002")
|
||||
)
|
||||
|
||||
assert not success
|
||||
assert "state" in message.lower()
|
||||
|
||||
def test_start_job(self):
|
||||
"""Test starting job"""
|
||||
# Create and fund contract
|
||||
success, _, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="job_006",
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0')
|
||||
)
|
||||
)
|
||||
|
||||
asyncio.run(self.escrow_manager.fund_contract(contract_id, "tx_hash_001"))
|
||||
|
||||
# Start job
|
||||
success, message = asyncio.run(self.escrow_manager.start_job(contract_id))
|
||||
|
||||
assert success, f"Job start failed: {message}"
|
||||
|
||||
# Check state
|
||||
contract = asyncio.run(self.escrow_manager.get_contract_info(contract_id))
|
||||
assert contract.state == EscrowState.JOB_STARTED
|
||||
|
||||
def test_complete_milestone(self):
|
||||
"""Test completing milestone"""
|
||||
milestones = [
|
||||
{
|
||||
'milestone_id': 'milestone_1',
|
||||
'description': 'Setup',
|
||||
'amount': Decimal('50.0')
|
||||
},
|
||||
{
|
||||
'milestone_id': 'milestone_2',
|
||||
'description': 'Delivery',
|
||||
'amount': Decimal('50.0')
|
||||
}
|
||||
]
|
||||
|
||||
# Create contract with milestones
|
||||
success, _, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="job_007",
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0'),
|
||||
milestones=milestones
|
||||
)
|
||||
)
|
||||
|
||||
asyncio.run(self.escrow_manager.fund_contract(contract_id, "tx_hash_001"))
|
||||
asyncio.run(self.escrow_manager.start_job(contract_id))
|
||||
|
||||
# Complete milestone
|
||||
success, message = asyncio.run(
|
||||
self.escrow_manager.complete_milestone(contract_id, "milestone_1")
|
||||
)
|
||||
|
||||
assert success, f"Milestone completion failed: {message}"
|
||||
|
||||
# Check milestone status
|
||||
contract = asyncio.run(self.escrow_manager.get_contract_info(contract_id))
|
||||
milestone = contract.milestones[0]
|
||||
assert milestone['completed']
|
||||
assert milestone['completed_at'] is not None
|
||||
|
||||
def test_verify_milestone(self):
|
||||
"""Test verifying milestone"""
|
||||
milestones = [
|
||||
{
|
||||
'milestone_id': 'milestone_1',
|
||||
'description': 'Setup',
|
||||
'amount': Decimal('50.0')
|
||||
}
|
||||
]
|
||||
|
||||
# Create contract with milestone
|
||||
success, _, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="job_008",
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0'),
|
||||
milestones=milestones
|
||||
)
|
||||
)
|
||||
|
||||
asyncio.run(self.escrow_manager.fund_contract(contract_id, "tx_hash_001"))
|
||||
asyncio.run(self.escrow_manager.start_job(contract_id))
|
||||
asyncio.run(self.escrow_manager.complete_milestone(contract_id, "milestone_1"))
|
||||
|
||||
# Verify milestone
|
||||
success, message = asyncio.run(
|
||||
self.escrow_manager.verify_milestone(contract_id, "milestone_1", True, "Work completed successfully")
|
||||
)
|
||||
|
||||
assert success, f"Milestone verification failed: {message}"
|
||||
|
||||
# Check verification status
|
||||
contract = asyncio.run(self.escrow_manager.get_contract_info(contract_id))
|
||||
milestone = contract.milestones[0]
|
||||
assert milestone['verified']
|
||||
assert milestone['verification_feedback'] == "Work completed successfully"
|
||||
|
||||
def test_create_dispute(self):
|
||||
"""Test creating dispute"""
|
||||
# Create and fund contract
|
||||
success, _, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="job_009",
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0')
|
||||
)
|
||||
)
|
||||
|
||||
asyncio.run(self.escrow_manager.fund_contract(contract_id, "tx_hash_001"))
|
||||
asyncio.run(self.escrow_manager.start_job(contract_id))
|
||||
|
||||
# Create dispute
|
||||
evidence = [
|
||||
{
|
||||
'type': 'screenshot',
|
||||
'description': 'Poor quality work',
|
||||
'timestamp': time.time()
|
||||
}
|
||||
]
|
||||
|
||||
success, message = asyncio.run(
|
||||
self.escrow_manager.create_dispute(
|
||||
contract_id, DisputeReason.QUALITY_ISSUES, "Work quality is poor", evidence
|
||||
)
|
||||
)
|
||||
|
||||
assert success, f"Dispute creation failed: {message}"
|
||||
|
||||
# Check dispute status
|
||||
contract = asyncio.run(self.escrow_manager.get_contract_info(contract_id))
|
||||
assert contract.state == EscrowState.DISPUTED
|
||||
assert contract.dispute_reason == DisputeReason.QUALITY_ISSUES
|
||||
|
||||
def test_resolve_dispute(self):
|
||||
"""Test resolving dispute"""
|
||||
# Create and fund contract
|
||||
success, _, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="job_010",
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0')
|
||||
)
|
||||
)
|
||||
|
||||
asyncio.run(self.escrow_manager.fund_contract(contract_id, "tx_hash_001"))
|
||||
asyncio.run(self.escrow_manager.start_job(contract_id))
|
||||
|
||||
# Create dispute
|
||||
asyncio.run(
|
||||
self.escrow_manager.create_dispute(
|
||||
contract_id, DisputeReason.QUALITY_ISSUES, "Quality issues"
|
||||
)
|
||||
)
|
||||
|
||||
# Resolve dispute
|
||||
resolution = {
|
||||
'winner': 'client',
|
||||
'client_refund': 0.8, # 80% refund
|
||||
'agent_payment': 0.2 # 20% payment
|
||||
}
|
||||
|
||||
success, message = asyncio.run(
|
||||
self.escrow_manager.resolve_dispute(contract_id, resolution)
|
||||
)
|
||||
|
||||
assert success, f"Dispute resolution failed: {message}"
|
||||
|
||||
# Check resolution
|
||||
contract = asyncio.run(self.escrow_manager.get_contract_info(contract_id))
|
||||
assert contract.state == EscrowState.RESOLVED
|
||||
assert contract.resolution == resolution
|
||||
|
||||
def test_refund_contract(self):
|
||||
"""Test refunding contract"""
|
||||
# Create and fund contract
|
||||
success, _, contract_id = asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id="job_011",
|
||||
client_address="0x1234567890123456789012345678901234567890",
|
||||
agent_address="0x2345678901234567890123456789012345678901",
|
||||
amount=Decimal('100.0')
|
||||
)
|
||||
)
|
||||
|
||||
asyncio.run(self.escrow_manager.fund_contract(contract_id, "tx_hash_001"))
|
||||
|
||||
# Refund contract
|
||||
success, message = asyncio.run(
|
||||
self.escrow_manager.refund_contract(contract_id, "Client requested refund")
|
||||
)
|
||||
|
||||
assert success, f"Refund failed: {message}"
|
||||
|
||||
# Check refund status
|
||||
contract = asyncio.run(self.escrow_manager.get_contract_info(contract_id))
|
||||
assert contract.state == EscrowState.REFUNDED
|
||||
assert contract.refunded_amount > 0
|
||||
|
||||
def test_get_escrow_statistics(self):
|
||||
"""Test getting escrow statistics"""
|
||||
# Create multiple contracts
|
||||
for i in range(5):
|
||||
asyncio.run(
|
||||
self.escrow_manager.create_contract(
|
||||
job_id=f"job_{i:03d}",
|
||||
client_address=f"0x123456789012345678901234567890123456789{i}",
|
||||
agent_address=f"0x234567890123456789012345678901234567890{i}",
|
||||
amount=Decimal('100.0')
|
||||
)
|
||||
)
|
||||
|
||||
stats = asyncio.run(self.escrow_manager.get_escrow_statistics())
|
||||
|
||||
assert 'total_contracts' in stats
|
||||
assert 'active_contracts' in stats
|
||||
assert 'disputed_contracts' in stats
|
||||
assert 'state_distribution' in stats
|
||||
assert 'total_amount' in stats
|
||||
assert stats['total_contracts'] >= 5
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
239
apps/blockchain-node/tests/economics/test_staking.py
Normal file
239
apps/blockchain-node/tests/economics/test_staking.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
Tests for Staking Mechanism
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from decimal import Decimal
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aitbc_chain.economics.staking import StakingManager, StakingStatus
|
||||
|
||||
class TestStakingManager:
|
||||
"""Test cases for staking manager"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup test environment"""
|
||||
self.staking_manager = StakingManager(min_stake_amount=1000.0)
|
||||
|
||||
# Register a test validator
|
||||
success, message = self.staking_manager.register_validator(
|
||||
"0xvalidator1", 2000.0, 0.05
|
||||
)
|
||||
assert success, f"Failed to register validator: {message}"
|
||||
|
||||
def test_register_validator(self):
|
||||
"""Test validator registration"""
|
||||
# Valid registration
|
||||
success, message = self.staking_manager.register_validator(
|
||||
"0xvalidator2", 1500.0, 0.03
|
||||
)
|
||||
assert success, f"Validator registration failed: {message}"
|
||||
|
||||
# Check validator info
|
||||
validator_info = self.staking_manager.get_validator_stake_info("0xvalidator2")
|
||||
assert validator_info is not None
|
||||
assert validator_info.validator_address == "0xvalidator2"
|
||||
assert float(validator_info.self_stake) == 1500.0
|
||||
assert validator_info.commission_rate == 0.03
|
||||
|
||||
def test_register_validator_insufficient_stake(self):
|
||||
"""Test validator registration with insufficient stake"""
|
||||
success, message = self.staking_manager.register_validator(
|
||||
"0xvalidator3", 500.0, 0.05
|
||||
)
|
||||
assert not success
|
||||
assert "insufficient stake" in message.lower()
|
||||
|
||||
def test_register_validator_invalid_commission(self):
|
||||
"""Test validator registration with invalid commission"""
|
||||
success, message = self.staking_manager.register_validator(
|
||||
"0xvalidator4", 1500.0, 0.15 # Too high
|
||||
)
|
||||
assert not success
|
||||
assert "commission" in message.lower()
|
||||
|
||||
def test_register_duplicate_validator(self):
|
||||
"""Test registering duplicate validator"""
|
||||
success, message = self.staking_manager.register_validator(
|
||||
"0xvalidator1", 2000.0, 0.05
|
||||
)
|
||||
assert not success
|
||||
assert "already registered" in message.lower()
|
||||
|
||||
def test_stake_to_validator(self):
|
||||
"""Test staking to validator"""
|
||||
success, message = self.staking_manager.stake(
|
||||
"0xvalidator1", "0xdelegator1", 1200.0
|
||||
)
|
||||
assert success, f"Staking failed: {message}"
|
||||
|
||||
# Check stake position
|
||||
position = self.staking_manager.get_stake_position("0xvalidator1", "0xdelegator1")
|
||||
assert position is not None
|
||||
assert position.validator_address == "0xvalidator1"
|
||||
assert position.delegator_address == "0xdelegator1"
|
||||
assert float(position.amount) == 1200.0
|
||||
assert position.status == StakingStatus.ACTIVE
|
||||
|
||||
def test_stake_insufficient_amount(self):
|
||||
"""Test staking insufficient amount"""
|
||||
success, message = self.staking_manager.stake(
|
||||
"0xvalidator1", "0xdelegator2", 500.0
|
||||
)
|
||||
assert not success
|
||||
assert "at least" in message.lower()
|
||||
|
||||
def test_stake_to_nonexistent_validator(self):
|
||||
"""Test staking to non-existent validator"""
|
||||
success, message = self.staking_manager.stake(
|
||||
"0xnonexistent", "0xdelegator3", 1200.0
|
||||
)
|
||||
assert not success
|
||||
assert "not found" in message.lower() or "not active" in message.lower()
|
||||
|
||||
def test_unstake(self):
|
||||
"""Test unstaking"""
|
||||
# First stake
|
||||
success, _ = self.staking_manager.stake("0xvalidator1", "0xdelegator4", 1200.0)
|
||||
assert success
|
||||
|
||||
# Then unstake
|
||||
success, message = self.staking_manager.unstake("0xvalidator1", "0xdelegator4")
|
||||
assert success, f"Unstaking failed: {message}"
|
||||
|
||||
# Check position status
|
||||
position = self.staking_manager.get_stake_position("0xvalidator1", "0xdelegator4")
|
||||
assert position is not None
|
||||
assert position.status == StakingStatus.UNSTAKING
|
||||
|
||||
def test_unstake_nonexistent_position(self):
|
||||
"""Test unstaking non-existent position"""
|
||||
success, message = self.staking_manager.unstake("0xvalidator1", "0xnonexistent")
|
||||
assert not success
|
||||
assert "not found" in message.lower()
|
||||
|
||||
def test_unstake_locked_position(self):
|
||||
"""Test unstaking locked position"""
|
||||
# Stake with long lock period
|
||||
success, _ = self.staking_manager.stake("0xvalidator1", "0xdelegator5", 1200.0, 90)
|
||||
assert success
|
||||
|
||||
# Try to unstake immediately
|
||||
success, message = self.staking_manager.unstake("0xvalidator1", "0xdelegator5")
|
||||
assert not success
|
||||
assert "lock period" in message.lower()
|
||||
|
||||
def test_withdraw(self):
|
||||
"""Test withdrawal after unstaking period"""
|
||||
# Stake and unstake
|
||||
success, _ = self.staking_manager.stake("0xvalidator1", "0xdelegator6", 1200.0, 1) # 1 day lock
|
||||
assert success
|
||||
|
||||
success, _ = self.staking_manager.unstake("0xvalidator1", "0xdelegator6")
|
||||
assert success
|
||||
|
||||
# Wait for unstaking period (simulate with direct manipulation)
|
||||
position = self.staking_manager.get_stake_position("0xvalidator1", "0xdelegator6")
|
||||
if position:
|
||||
position.staked_at = time.time() - (2 * 24 * 3600) # 2 days ago
|
||||
|
||||
# Withdraw
|
||||
success, message, amount = self.staking_manager.withdraw("0xvalidator1", "0xdelegator6")
|
||||
assert success, f"Withdrawal failed: {message}"
|
||||
assert amount == 1200.0 # Should get back the full amount
|
||||
|
||||
# Check position status
|
||||
position = self.staking_manager.get_stake_position("0xvalidator1", "0xdelegator6")
|
||||
assert position is not None
|
||||
assert position.status == StakingStatus.WITHDRAWN
|
||||
|
||||
def test_withdraw_too_early(self):
|
||||
"""Test withdrawal before unstaking period completes"""
|
||||
# Stake and unstake
|
||||
success, _ = self.staking_manager.stake("0xvalidator1", "0xdelegator7", 1200.0, 30) # 30 days
|
||||
assert success
|
||||
|
||||
success, _ = self.staking_manager.unstake("0xvalidator1", "0xdelegator7")
|
||||
assert success
|
||||
|
||||
# Try to withdraw immediately
|
||||
success, message, amount = self.staking_manager.withdraw("0xvalidator1", "0xdelegator7")
|
||||
assert not success
|
||||
assert "not completed" in message.lower()
|
||||
assert amount == 0.0
|
||||
|
||||
def test_slash_validator(self):
|
||||
"""Test validator slashing"""
|
||||
# Stake to validator
|
||||
success, _ = self.staking_manager.stake("0xvalidator1", "0xdelegator8", 1200.0)
|
||||
assert success
|
||||
|
||||
# Slash validator
|
||||
success, message = self.staking_manager.slash_validator("0xvalidator1", 0.1, "Test slash")
|
||||
assert success, f"Slashing failed: {message}"
|
||||
|
||||
# Check stake reduction
|
||||
position = self.staking_manager.get_stake_position("0xvalidator1", "0xdelegator8")
|
||||
assert position is not None
|
||||
assert float(position.amount) == 1080.0 # 10% reduction
|
||||
assert position.slash_count == 1
|
||||
|
||||
def test_get_validator_stake_info(self):
|
||||
"""Test getting validator stake information"""
|
||||
# Add delegators
|
||||
self.staking_manager.stake("0xvalidator1", "0xdelegator9", 1000.0)
|
||||
self.staking_manager.stake("0xvalidator1", "0xdelegator10", 1500.0)
|
||||
|
||||
info = self.staking_manager.get_validator_stake_info("0xvalidator1")
|
||||
assert info is not None
|
||||
assert float(info.self_stake) == 2000.0
|
||||
assert float(info.delegated_stake) == 2500.0
|
||||
assert float(info.total_stake) == 4500.0
|
||||
assert info.delegators_count == 2
|
||||
|
||||
def test_get_all_validators(self):
|
||||
"""Test getting all validators"""
|
||||
# Register another validator
|
||||
self.staking_manager.register_validator("0xvalidator5", 1800.0, 0.04)
|
||||
|
||||
validators = self.staking_manager.get_all_validators()
|
||||
assert len(validators) >= 2
|
||||
|
||||
validator_addresses = [v.validator_address for v in validators]
|
||||
assert "0xvalidator1" in validator_addresses
|
||||
assert "0xvalidator5" in validator_addresses
|
||||
|
||||
def test_get_active_validators(self):
|
||||
"""Test getting active validators only"""
|
||||
# Unregister one validator
|
||||
self.staking_manager.unregister_validator("0xvalidator1")
|
||||
|
||||
active_validators = self.staking_manager.get_active_validators()
|
||||
validator_addresses = [v.validator_address for v in active_validators]
|
||||
|
||||
assert "0xvalidator1" not in validator_addresses
|
||||
|
||||
def test_get_total_staked(self):
|
||||
"""Test getting total staked amount"""
|
||||
# Add some stakes
|
||||
self.staking_manager.stake("0xvalidator1", "0xdelegator11", 1000.0)
|
||||
self.staking_manager.stake("0xvalidator1", "0xdelegator12", 2000.0)
|
||||
|
||||
total = self.staking_manager.get_total_staked()
|
||||
expected = 2000.0 + 1000.0 + 2000.0 + 2000.0 # validator1 self-stake + delegators
|
||||
assert float(total) == expected
|
||||
|
||||
def test_get_staking_statistics(self):
|
||||
"""Test staking statistics"""
|
||||
stats = self.staking_manager.get_staking_statistics()
|
||||
|
||||
assert 'total_validators' in stats
|
||||
assert 'total_staked' in stats
|
||||
assert 'total_delegators' in stats
|
||||
assert 'average_stake_per_validator' in stats
|
||||
assert stats['total_validators'] >= 1
|
||||
assert stats['total_staked'] >= 2000.0 # At least the initial validator stake
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
101
apps/blockchain-node/tests/network/test_discovery.py
Normal file
101
apps/blockchain-node/tests/network/test_discovery.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Tests for P2P Discovery Service
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aitbc_chain.network.discovery import P2PDiscovery, PeerNode, NodeStatus
|
||||
|
||||
class TestP2PDiscovery:
|
||||
"""Test cases for P2P discovery service"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup test environment"""
|
||||
self.discovery = P2PDiscovery("test-node", "127.0.0.1", 8000)
|
||||
|
||||
# Add bootstrap nodes
|
||||
self.discovery.add_bootstrap_node("127.0.0.1", 8001)
|
||||
self.discovery.add_bootstrap_node("127.0.0.1", 8002)
|
||||
|
||||
def test_generate_node_id(self):
|
||||
"""Test node ID generation"""
|
||||
address = "127.0.0.1"
|
||||
port = 8000
|
||||
public_key = "test_public_key"
|
||||
|
||||
node_id = self.discovery.generate_node_id(address, port, public_key)
|
||||
|
||||
assert isinstance(node_id, str)
|
||||
assert len(node_id) == 64 # SHA256 hex length
|
||||
|
||||
# Test consistency
|
||||
node_id2 = self.discovery.generate_node_id(address, port, public_key)
|
||||
assert node_id == node_id2
|
||||
|
||||
def test_add_bootstrap_node(self):
|
||||
"""Test adding bootstrap node"""
|
||||
initial_count = len(self.discovery.bootstrap_nodes)
|
||||
|
||||
self.discovery.add_bootstrap_node("127.0.0.1", 8003)
|
||||
|
||||
assert len(self.discovery.bootstrap_nodes) == initial_count + 1
|
||||
assert ("127.0.0.1", 8003) in self.discovery.bootstrap_nodes
|
||||
|
||||
def test_generate_node_id_consistency(self):
|
||||
"""Test node ID generation consistency"""
|
||||
address = "192.168.1.1"
|
||||
port = 9000
|
||||
public_key = "test_key"
|
||||
|
||||
node_id1 = self.discovery.generate_node_id(address, port, public_key)
|
||||
node_id2 = self.discovery.generate_node_id(address, port, public_key)
|
||||
|
||||
assert node_id1 == node_id2
|
||||
|
||||
# Different inputs should produce different IDs
|
||||
node_id3 = self.discovery.generate_node_id("192.168.1.2", port, public_key)
|
||||
assert node_id1 != node_id3
|
||||
|
||||
def test_get_peer_count_empty(self):
|
||||
"""Test getting peer count with no peers"""
|
||||
assert self.discovery.get_peer_count() == 0
|
||||
|
||||
def test_get_peer_list_empty(self):
|
||||
"""Test getting peer list with no peers"""
|
||||
assert self.discovery.get_peer_list() == []
|
||||
|
||||
def test_update_peer_reputation_new_peer(self):
|
||||
"""Test updating reputation for non-existent peer"""
|
||||
result = self.discovery.update_peer_reputation("nonexistent", 0.1)
|
||||
assert result is False
|
||||
|
||||
def test_update_peer_reputation_bounds(self):
|
||||
"""Test reputation bounds"""
|
||||
# Add a test peer
|
||||
peer = PeerNode(
|
||||
node_id="test_peer",
|
||||
address="127.0.0.1",
|
||||
port=8001,
|
||||
public_key="test_key",
|
||||
last_seen=0,
|
||||
status=NodeStatus.ONLINE,
|
||||
capabilities=["test"],
|
||||
reputation=0.5,
|
||||
connection_count=0
|
||||
)
|
||||
self.discovery.peers["test_peer"] = peer
|
||||
|
||||
# Try to increase beyond 1.0
|
||||
result = self.discovery.update_peer_reputation("test_peer", 0.6)
|
||||
assert result is True
|
||||
assert self.discovery.peers["test_peer"].reputation == 1.0
|
||||
|
||||
# Try to decrease below 0.0
|
||||
result = self.discovery.update_peer_reputation("test_peer", -1.5)
|
||||
assert result is True
|
||||
assert self.discovery.peers["test_peer"].reputation == 0.0
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
Reference in New Issue
Block a user