Files
aitbc/apps/blockchain-node/tests/test_sync.py
aitbc e60cc3226c
Some checks failed
API Endpoint Tests / test-api-endpoints (push) Successful in 9s
Blockchain Synchronization Verification / sync-verification (push) Failing after 1s
CLI Tests / test-cli (push) Failing after 3s
Documentation Validation / validate-docs (push) Successful in 6s
Documentation Validation / validate-policies-strict (push) Successful in 2s
Integration Tests / test-service-integration (push) Successful in 40s
Multi-Node Blockchain Health Monitoring / health-check (push) Successful in 1s
P2P Network Verification / p2p-verification (push) Successful in 2s
Production Tests / Production Integration Tests (push) Successful in 21s
Python Tests / test-python (push) Successful in 13s
Security Scanning / security-scan (push) Failing after 46s
Smart Contract Tests / test-solidity (map[name:aitbc-token path:packages/solidity/aitbc-token]) (push) Successful in 17s
Smart Contract Tests / lint-solidity (push) Successful in 10s
Add sys import to test files and remove obsolete integration tests
- Add sys import to 29 test files across agent-coordinator, blockchain-event-bridge, blockchain-node, and coordinator-api
- Remove apps/blockchain-event-bridge/tests/test_integration.py (obsolete bridge integration tests)
- Remove apps/coordinator-api/tests/test_integration.py (obsolete API integration tests)
- Implement GPU registration in marketplace_gpu.py with GPURegistry model persistence
2026-04-23 16:43:17 +02:00

343 lines
14 KiB
Python
Executable File

"""Tests for chain synchronization, conflict resolution, and signature validation."""
import hashlib
import time
import sys
import pytest
from datetime import datetime
from contextlib import contextmanager
from sqlmodel import Session, SQLModel, create_engine, select
from aitbc_chain.models import Block, Transaction
from aitbc_chain.metrics import metrics_registry
from aitbc_chain.sync import ChainSync, ProposerSignatureValidator, ImportResult
@pytest.fixture(autouse=True)
def reset_metrics():
metrics_registry.reset()
yield
metrics_registry.reset()
@pytest.fixture
def db_engine(tmp_path):
db_path = tmp_path / "test_sync.db"
engine = create_engine(f"sqlite:///{db_path}", echo=False)
SQLModel.metadata.create_all(engine)
return engine
@pytest.fixture
def session_factory(db_engine):
@contextmanager
def _factory():
with Session(db_engine) as session:
yield session
return _factory
def _make_block_hash(chain_id, height, parent_hash, timestamp):
payload = f"{chain_id}|{height}|{parent_hash}|{timestamp.isoformat()}".encode()
return "0x" + hashlib.sha256(payload).hexdigest()
def _seed_chain(session_factory, count=5, chain_id="test-chain", proposer="proposer-a"):
"""Seed a chain with `count` blocks."""
parent_hash = "0x00"
blocks = []
with session_factory() as session:
for h in range(count):
ts = datetime(2026, 1, 1, 0, 0, h)
bh = _make_block_hash(chain_id, h, parent_hash, ts)
block = Block(
chain_id=chain_id,
height=h, hash=bh, parent_hash=parent_hash,
proposer=proposer, timestamp=ts, tx_count=0,
)
session.add(block)
blocks.append({"height": h, "hash": bh, "parent_hash": parent_hash,
"proposer": proposer, "timestamp": ts.isoformat()})
parent_hash = bh
session.commit()
return blocks
class TestProposerSignatureValidator:
def test_valid_block(self):
v = ProposerSignatureValidator()
ts = datetime.utcnow()
bh = _make_block_hash("test", 1, "0x00", ts)
ok, reason = v.validate_block_signature({
"height": 1, "hash": bh, "parent_hash": "0x00",
"proposer": "node-a", "timestamp": ts.isoformat(),
})
assert ok is True
assert reason == "Valid"
def test_missing_proposer(self):
v = ProposerSignatureValidator()
ok, reason = v.validate_block_signature({
"height": 1, "hash": "0x" + "a" * 64, "parent_hash": "0x00",
"timestamp": datetime.utcnow().isoformat(),
})
assert ok is False
assert "Missing proposer" in reason
def test_invalid_hash_format(self):
v = ProposerSignatureValidator()
ok, reason = v.validate_block_signature({
"height": 1, "hash": "badhash", "parent_hash": "0x00",
"proposer": "node-a", "timestamp": datetime.utcnow().isoformat(),
})
assert ok is False
assert "Invalid block hash" in reason
def test_invalid_hash_length(self):
v = ProposerSignatureValidator()
ok, reason = v.validate_block_signature({
"height": 1, "hash": "0xabc", "parent_hash": "0x00",
"proposer": "node-a", "timestamp": datetime.utcnow().isoformat(),
})
assert ok is False
assert "Invalid hash length" in reason
def test_untrusted_proposer_rejected(self):
v = ProposerSignatureValidator(trusted_proposers=["node-a", "node-b"])
ts = datetime.utcnow()
bh = _make_block_hash("test", 1, "0x00", ts)
ok, reason = v.validate_block_signature({
"height": 1, "hash": bh, "parent_hash": "0x00",
"proposer": "node-evil", "timestamp": ts.isoformat(),
})
assert ok is False
assert "not in trusted set" in reason
def test_trusted_proposer_accepted(self):
v = ProposerSignatureValidator(trusted_proposers=["node-a"])
ts = datetime.utcnow()
bh = _make_block_hash("test", 1, "0x00", ts)
ok, reason = v.validate_block_signature({
"height": 1, "hash": bh, "parent_hash": "0x00",
"proposer": "node-a", "timestamp": ts.isoformat(),
})
assert ok is True
def test_add_remove_trusted(self):
v = ProposerSignatureValidator()
assert len(v.trusted_proposers) == 0
v.add_trusted("node-x")
assert "node-x" in v.trusted_proposers
v.remove_trusted("node-x")
assert "node-x" not in v.trusted_proposers
def test_missing_required_field(self):
v = ProposerSignatureValidator()
ok, reason = v.validate_block_signature({
"hash": "0x" + "a" * 64, "proposer": "node-a",
# missing height, parent_hash, timestamp
})
assert ok is False
assert "Missing required field" in reason
class TestChainSyncAppend:
def test_append_to_empty_chain(self, session_factory):
sync = ChainSync(session_factory, chain_id="test", validate_signatures=False)
ts = datetime.utcnow()
bh = _make_block_hash("test", 0, "0x00", ts)
result = sync.import_block({
"height": 0, "hash": bh, "parent_hash": "0x00",
"proposer": "node-a", "timestamp": ts.isoformat(),
})
assert result.accepted is True
assert result.height == 0
def test_append_sequential(self, session_factory):
sync = ChainSync(session_factory, chain_id="test", validate_signatures=False)
blocks = _seed_chain(session_factory, count=3, chain_id="test")
last = blocks[-1]
ts = datetime(2026, 1, 1, 0, 0, 3)
bh = _make_block_hash("test", 3, last["hash"], ts)
result = sync.import_block({
"height": 3, "hash": bh, "parent_hash": last["hash"],
"proposer": "node-a", "timestamp": ts.isoformat(),
})
assert result.accepted is True
assert result.height == 3
def test_duplicate_rejected(self, session_factory):
sync = ChainSync(session_factory, chain_id="test", validate_signatures=False)
blocks = _seed_chain(session_factory, count=2, chain_id="test")
result = sync.import_block({
"height": 0, "hash": blocks[0]["hash"], "parent_hash": "0x00",
"proposer": "proposer-a", "timestamp": blocks[0]["timestamp"],
})
assert result.accepted is False
assert "already exists" in result.reason
def test_stale_block_rejected(self, session_factory):
sync = ChainSync(session_factory, chain_id="test", validate_signatures=False)
_seed_chain(session_factory, count=5, chain_id="test")
ts = datetime(2026, 6, 1)
bh = _make_block_hash("test", 2, "0x00", ts)
result = sync.import_block({
"height": 2, "hash": bh, "parent_hash": "0x00",
"proposer": "node-b", "timestamp": ts.isoformat(),
})
assert result.accepted is False
assert "Stale" in result.reason or "Fork" in result.reason or "longer" in result.reason
def test_gap_detected(self, session_factory):
sync = ChainSync(session_factory, chain_id="test", validate_signatures=False)
_seed_chain(session_factory, count=3, chain_id="test")
ts = datetime(2026, 6, 1)
bh = _make_block_hash("test", 10, "0x00", ts)
result = sync.import_block({
"height": 10, "hash": bh, "parent_hash": "0x00",
"proposer": "node-a", "timestamp": ts.isoformat(),
})
assert result.accepted is False
assert "Gap" in result.reason
def test_append_with_transactions(self, session_factory):
sync = ChainSync(session_factory, chain_id="test", validate_signatures=False)
blocks = _seed_chain(session_factory, count=1, chain_id="test")
last = blocks[-1]
ts = datetime(2026, 1, 1, 0, 0, 1)
bh = _make_block_hash("test", 1, last["hash"], ts)
txs = [
{"tx_hash": "0x" + "a" * 64, "sender": "alice", "recipient": "bob"},
{"tx_hash": "0x" + "b" * 64, "sender": "charlie", "recipient": "dave"},
]
result = sync.import_block({
"height": 1, "hash": bh, "parent_hash": last["hash"],
"proposer": "node-a", "timestamp": ts.isoformat(), "tx_count": 2,
}, transactions=txs)
assert result.accepted is True
# Verify transactions were stored
with session_factory() as session:
stored_txs = session.exec(select(Transaction).where(Transaction.block_height == 1)).all()
assert len(stored_txs) == 2
class TestChainSyncSignatureValidation:
def test_untrusted_proposer_rejected_on_import(self, session_factory):
validator = ProposerSignatureValidator(trusted_proposers=["node-a"])
sync = ChainSync(session_factory, chain_id="test", validator=validator, validate_signatures=True)
ts = datetime.utcnow()
bh = _make_block_hash("test", 0, "0x00", ts)
result = sync.import_block({
"height": 0, "hash": bh, "parent_hash": "0x00",
"proposer": "node-evil", "timestamp": ts.isoformat(),
})
assert result.accepted is False
assert "not in trusted set" in result.reason
def test_trusted_proposer_accepted_on_import(self, session_factory):
validator = ProposerSignatureValidator(trusted_proposers=["node-a"])
sync = ChainSync(session_factory, chain_id="test", validator=validator, validate_signatures=True)
ts = datetime.utcnow()
bh = _make_block_hash("test", 0, "0x00", ts)
result = sync.import_block({
"height": 0, "hash": bh, "parent_hash": "0x00",
"proposer": "node-a", "timestamp": ts.isoformat(),
})
assert result.accepted is True
def test_validation_disabled(self, session_factory):
validator = ProposerSignatureValidator(trusted_proposers=["node-a"])
sync = ChainSync(session_factory, chain_id="test", validator=validator, validate_signatures=False)
ts = datetime.utcnow()
bh = _make_block_hash("test", 0, "0x00", ts)
result = sync.import_block({
"height": 0, "hash": bh, "parent_hash": "0x00",
"proposer": "node-evil", "timestamp": ts.isoformat(),
})
assert result.accepted is True # validation disabled
class TestChainSyncConflictResolution:
def test_fork_at_same_height_rejected(self, session_factory):
"""Fork at same height as our chain — our chain wins (equal length)."""
sync = ChainSync(session_factory, chain_id="test", validate_signatures=False)
blocks = _seed_chain(session_factory, count=5, chain_id="test")
# Try to import a different block at height 3
ts = datetime(2026, 6, 15)
bh = _make_block_hash("test", 3, "0xdifferent", ts)
result = sync.import_block({
"height": 3, "hash": bh, "parent_hash": "0xdifferent",
"proposer": "node-b", "timestamp": ts.isoformat(),
})
assert result.accepted is False
assert "longer" in result.reason or "Fork" in result.reason
def test_sync_status(self, session_factory):
sync = ChainSync(session_factory, chain_id="test-chain", validate_signatures=False)
_seed_chain(session_factory, count=5, chain_id="test-chain")
status = sync.get_sync_status()
assert status["chain_id"] == "test-chain"
assert status["head_height"] == 4
assert status["total_blocks"] == 5
assert status["max_reorg_depth"] == 10
class TestSyncMetrics:
def test_accepted_block_increments_metrics(self, session_factory):
sync = ChainSync(session_factory, chain_id="test", validate_signatures=False)
ts = datetime.utcnow()
bh = _make_block_hash("test", 0, "0x00", ts)
sync.import_block({
"height": 0, "hash": bh, "parent_hash": "0x00",
"proposer": "node-a", "timestamp": ts.isoformat(),
})
prom = metrics_registry.render_prometheus()
assert "sync_blocks_received_total" in prom
assert "sync_blocks_accepted_total" in prom
def test_rejected_block_increments_metrics(self, session_factory):
validator = ProposerSignatureValidator(trusted_proposers=["node-a"])
sync = ChainSync(session_factory, chain_id="test", validator=validator, validate_signatures=True)
ts = datetime.utcnow()
bh = _make_block_hash("test", 0, "0x00", ts)
sync.import_block({
"height": 0, "hash": bh, "parent_hash": "0x00",
"proposer": "node-evil", "timestamp": ts.isoformat(),
})
prom = metrics_registry.render_prometheus()
assert "sync_blocks_rejected_total" in prom
def test_duplicate_increments_metrics(self, session_factory):
sync = ChainSync(session_factory, chain_id="test", validate_signatures=False)
_seed_chain(session_factory, count=1, chain_id="test")
with session_factory() as session:
block = session.exec(select(Block).where(Block.height == 0)).first()
sync.import_block({
"height": 0, "hash": block.hash, "parent_hash": "0x00",
"proposer": "proposer-a", "timestamp": block.timestamp.isoformat(),
})
prom = metrics_registry.render_prometheus()
assert "sync_blocks_duplicate_total" in prom
def test_fork_increments_metrics(self, session_factory):
sync = ChainSync(session_factory, chain_id="test", validate_signatures=False)
_seed_chain(session_factory, count=5, chain_id="test")
ts = datetime(2026, 6, 15)
bh = _make_block_hash("test", 3, "0xdifferent", ts)
sync.import_block({
"height": 3, "hash": bh, "parent_hash": "0xdifferent",
"proposer": "node-b", "timestamp": ts.isoformat(),
})
prom = metrics_registry.render_prometheus()
assert "sync_forks_detected_total" in prom