Files
aitbc/apps/blockchain-node/tests/test_force_sync_endpoints.py
aitbc d3415413b3
Some checks failed
Integration Tests / test-service-integration (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
API Endpoint Tests / test-api-endpoints (push) Has been cancelled
Add hash conflict detection and cleanup across chains in import operations and improve database initialization
- Add hash conflict detection in import_block to delete existing blocks with same hash
- Add hash conflict cleanup in import_chain before importing blocks
- Add logging for hash conflict deletions showing affected chains
- Add WAL file permission setting in init_db for .db-shm and .db-wal files
- Add test_import_chain_clears_hash_conflicts_across_chains to verify cross-chain hash cleanup
2026-04-15 08:17:35 +02:00

343 lines
11 KiB
Python

import hashlib
from contextlib import contextmanager
from datetime import datetime
import pytest
from sqlmodel import Session, SQLModel, create_engine, select
from aitbc_chain.models import Account, Block, Transaction
from aitbc_chain.rpc import router as rpc_router
def _hex(value: str) -> str:
return "0x" + hashlib.sha256(value.encode()).hexdigest()
@pytest.fixture
def isolated_engine(tmp_path, monkeypatch):
db_path = tmp_path / "test_force_sync_endpoints.db"
engine = create_engine(f"sqlite:///{db_path}", echo=False)
SQLModel.metadata.create_all(engine)
@contextmanager
def _session_scope():
with Session(engine) as session:
yield session
monkeypatch.setattr(rpc_router, "session_scope", _session_scope)
return engine
@pytest.mark.asyncio
async def test_export_chain_filters_records_by_chain_id(isolated_engine):
with Session(isolated_engine) as session:
session.add(
Block(
chain_id="chain-a",
height=0,
hash=_hex("chain-a-block-0"),
parent_hash="0x00",
proposer="node-a",
timestamp=datetime(2026, 1, 1, 0, 0, 0),
tx_count=1,
)
)
session.add(
Block(
chain_id="chain-a",
height=1,
hash=_hex("chain-a-block-1"),
parent_hash=_hex("chain-a-block-0"),
proposer="node-a",
timestamp=datetime(2026, 1, 1, 0, 0, 1),
tx_count=0,
)
)
session.add(
Block(
chain_id="chain-b",
height=0,
hash=_hex("chain-b-block-0"),
parent_hash="0x00",
proposer="node-b",
timestamp=datetime(2026, 1, 1, 0, 0, 2),
tx_count=1,
)
)
session.add(Account(chain_id="chain-a", address="alice", balance=10, nonce=1))
session.add(Account(chain_id="chain-b", address="mallory", balance=99, nonce=5))
session.add(
Transaction(
chain_id="chain-a",
tx_hash=_hex("chain-a-tx"),
block_height=0,
sender="alice",
recipient="bob",
payload={"kind": "payment"},
value=7,
fee=1,
nonce=2,
status="confirmed",
timestamp="2026-01-01T00:00:00",
tx_metadata="meta-a",
)
)
session.add(
Transaction(
chain_id="chain-b",
tx_hash=_hex("chain-b-tx"),
block_height=0,
sender="mallory",
recipient="eve",
payload={"kind": "payment"},
value=3,
fee=1,
nonce=1,
status="confirmed",
timestamp="2026-01-01T00:00:02",
tx_metadata="meta-b",
)
)
session.commit()
result = await rpc_router.export_chain(chain_id="chain-a")
assert result["success"] is True
assert result["export_data"]["chain_id"] == "chain-a"
assert [block["height"] for block in result["export_data"]["blocks"]] == [0, 1]
assert {block["chain_id"] for block in result["export_data"]["blocks"]} == {"chain-a"}
assert len(result["export_data"]["accounts"]) == 1
assert len(result["export_data"]["transactions"]) == 1
assert result["export_data"]["transactions"][0]["tx_hash"] == _hex("chain-a-tx")
assert result["export_data"]["transactions"][0]["payload"] == {"kind": "payment"}
@pytest.mark.asyncio
async def test_import_chain_dedupes_duplicate_heights_and_preserves_transaction_fields(isolated_engine):
with Session(isolated_engine) as session:
session.add(
Block(
chain_id="chain-a",
height=0,
hash=_hex("old-chain-a-block"),
parent_hash="0x00",
proposer="node-a",
timestamp=datetime(2025, 12, 31, 23, 59, 59),
tx_count=0,
)
)
session.add(Account(chain_id="chain-a", address="alice", balance=1, nonce=0))
session.add(
Transaction(
chain_id="chain-a",
tx_hash=_hex("old-chain-a-tx"),
block_height=0,
sender="alice",
recipient="bob",
payload={"kind": "payment"},
value=1,
fee=1,
nonce=0,
status="pending",
timestamp="2025-12-31T23:59:59",
tx_metadata="old",
)
)
session.add(
Block(
chain_id="chain-b",
height=0,
hash=_hex("chain-b-existing-block"),
parent_hash="0x00",
proposer="node-b",
timestamp=datetime(2026, 1, 1, 0, 0, 0),
tx_count=0,
)
)
session.commit()
import_payload = {
"chain_id": "chain-a",
"blocks": [
{
"chain_id": "chain-a",
"height": 0,
"hash": _hex("incoming-block-0-old"),
"parent_hash": "0x00",
"proposer": "node-a",
"timestamp": "2026-01-02T00:00:00",
"tx_count": 0,
},
{
"chain_id": "chain-a",
"height": 0,
"hash": _hex("incoming-block-0-new"),
"parent_hash": "0x00",
"proposer": "node-a",
"timestamp": "2026-01-02T00:00:01",
"tx_count": 1,
},
{
"chain_id": "chain-a",
"height": 1,
"hash": _hex("incoming-block-1"),
"parent_hash": _hex("incoming-block-0-new"),
"proposer": "node-a",
"timestamp": "2026-01-02T00:00:02",
"tx_count": 1,
},
],
"accounts": [
{"chain_id": "chain-a", "address": "alice", "balance": 25, "nonce": 2}
],
"transactions": [
{
"chain_id": "chain-a",
"tx_hash": _hex("incoming-tx-1"),
"block_height": 1,
"sender": "alice",
"recipient": "bob",
"payload": {"kind": "payment"},
"value": 10,
"fee": 1,
"nonce": 2,
"timestamp": "2026-01-02T00:00:02",
"status": "confirmed",
"created_at": "2026-01-02T00:00:02",
"tx_metadata": "new",
}
],
}
result = await rpc_router.import_chain(import_payload)
assert result["success"] is True
assert result["imported_blocks"] == 2
assert result["imported_transactions"] == 1
with Session(isolated_engine) as session:
chain_a_blocks = session.exec(
select(Block).where(Block.chain_id == "chain-a").order_by(Block.height)
).all()
chain_b_blocks = session.exec(
select(Block).where(Block.chain_id == "chain-b").order_by(Block.height)
).all()
chain_a_accounts = session.exec(
select(Account).where(Account.chain_id == "chain-a")
).all()
chain_a_transactions = session.exec(
select(Transaction).where(Transaction.chain_id == "chain-a")
).all()
assert [block.height for block in chain_a_blocks] == [0, 1]
assert chain_a_blocks[0].hash == _hex("incoming-block-0-new")
assert len(chain_b_blocks) == 1
assert chain_b_blocks[0].hash == _hex("chain-b-existing-block")
assert len(chain_a_accounts) == 1
assert chain_a_accounts[0].balance == 25
assert len(chain_a_transactions) == 1
assert chain_a_transactions[0].tx_hash == _hex("incoming-tx-1")
assert chain_a_transactions[0].timestamp == "2026-01-02T00:00:02"
async def test_import_chain_clears_hash_conflicts_across_chains(isolated_engine):
"""Test that import-chain clears blocks with conflicting hashes across different chains."""
from aitbc_chain.rpc import router as rpc_router
from aitbc_chain.database import get_engine
with Session(isolated_engine) as session:
session.add(
Block(
chain_id="chain-a",
height=0,
hash=_hex("chain-a-block-0"),
parent_hash="0x00",
proposer="node-a",
timestamp=datetime(2026, 1, 1, 0, 0, 0),
tx_count=0,
)
)
session.add(
Block(
chain_id="chain-a",
height=1,
hash=_hex("chain-a-block-1"),
parent_hash=_hex("chain-a-block-0"),
proposer="node-a",
timestamp=datetime(2026, 1, 1, 0, 0, 1),
tx_count=0,
)
)
session.add(
Block(
chain_id="chain-b",
height=0,
hash=_hex("chain-b-block-0"),
parent_hash="0x00",
proposer="node-b",
timestamp=datetime(2026, 1, 1, 0, 0, 0),
tx_count=0,
)
)
session.add(
Block(
chain_id="chain-b",
height=1,
hash=_hex("chain-b-block-1"),
parent_hash=_hex("chain-b-block-0"),
proposer="node-b",
timestamp=datetime(2026, 1, 1, 0, 0, 1),
tx_count=0,
)
)
session.commit()
with Session(isolated_engine) as session:
chain_a_blocks = session.exec(
select(Block).where(Block.chain_id == "chain-a").order_by(Block.height)
).all()
conflicting_hash = chain_a_blocks[0].hash
import_payload = {
"chain_id": "chain-c",
"blocks": [
{
"chain_id": "chain-c",
"height": 0,
"hash": conflicting_hash,
"parent_hash": _hex("parent-0"),
"proposer": _hex("proposer-0"),
"timestamp": "2026-01-01T00:00:00",
"tx_count": 0,
},
{
"chain_id": "chain-c",
"height": 1,
"hash": _hex("chain-c-block-1"),
"parent_hash": conflicting_hash,
"proposer": _hex("proposer-1"),
"timestamp": "2026-01-01T00:00:01",
"tx_count": 0,
},
],
}
result = await rpc_router.import_chain(import_payload)
assert result["success"] is True
assert result["imported_blocks"] == 2
with Session(isolated_engine) as session:
chain_c_blocks = session.exec(
select(Block).where(Block.chain_id == "chain-c").order_by(Block.height)
).all()
chain_a_blocks_after = session.exec(
select(Block).where(Block.chain_id == "chain-a").order_by(Block.height)
).all()
assert [block.height for block in chain_c_blocks] == [0, 1]
assert chain_c_blocks[0].hash == conflicting_hash
assert len(chain_a_blocks_after) == 1
assert chain_a_blocks_after[0].height == 1