feat: add foreign key constraints and metrics for blockchain node

This commit is contained in:
oib
2025-09-28 06:04:30 +02:00
parent c1926136fb
commit fb60505cdf
189 changed files with 15678 additions and 158 deletions

View File

@ -0,0 +1,23 @@
from __future__ import annotations
import pytest
from sqlmodel import SQLModel, Session, create_engine
from aitbc_chain.models import Block, Transaction, Receipt # noqa: F401 - ensure models imported for metadata
@pytest.fixture(name="engine")
def engine_fixture():
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
SQLModel.metadata.create_all(engine)
try:
yield engine
finally:
SQLModel.metadata.drop_all(engine)
@pytest.fixture(name="session")
def session_fixture(engine):
with Session(engine) as session:
yield session
session.rollback()

View File

@ -0,0 +1,76 @@
from __future__ import annotations
import asyncio
import pytest
from fastapi.testclient import TestClient
from aitbc_chain.app import create_app
from aitbc_chain.gossip import BroadcastGossipBackend, InMemoryGossipBackend, gossip_broker
@pytest.fixture(autouse=True)
async def reset_broker_backend():
previous_backend = InMemoryGossipBackend()
await gossip_broker.set_backend(previous_backend)
yield
await gossip_broker.set_backend(InMemoryGossipBackend())
def _run_in_thread(fn):
loop = asyncio.get_event_loop()
return loop.run_in_executor(None, fn)
@pytest.mark.asyncio
async def test_websocket_fanout_with_broadcast_backend():
backend = BroadcastGossipBackend("memory://")
await gossip_broker.set_backend(backend)
app = create_app()
loop = asyncio.get_running_loop()
def _sync_test() -> None:
with TestClient(app) as client:
with client.websocket_connect("/rpc/ws/transactions") as ws_a, client.websocket_connect(
"/rpc/ws/transactions"
) as ws_b:
payload = {
"tx_hash": "0x01",
"sender": "alice",
"recipient": "bob",
"payload": {"amount": 1},
"nonce": 0,
"fee": 0,
"type": "TRANSFER",
}
fut = asyncio.run_coroutine_threadsafe(gossip_broker.publish("transactions", payload), loop)
fut.result(timeout=5.0)
assert ws_a.receive_json() == payload
assert ws_b.receive_json() == payload
await _run_in_thread(_sync_test)
@pytest.mark.asyncio
async def test_broadcast_backend_decodes_cursorless_payload():
backend = BroadcastGossipBackend("memory://")
await gossip_broker.set_backend(backend)
app = create_app()
loop = asyncio.get_running_loop()
def _sync_test() -> None:
with TestClient(app) as client:
with client.websocket_connect("/rpc/ws/blocks") as ws:
payload = [
{"height": 1, "hash": "0xabc"},
{"height": 2, "hash": "0xdef"},
]
fut = asyncio.run_coroutine_threadsafe(gossip_broker.publish("blocks", payload), loop)
fut.result(timeout=5.0)
assert ws.receive_json() == payload
await _run_in_thread(_sync_test)

View File

@ -0,0 +1,92 @@
from __future__ import annotations
import pytest
from sqlmodel import Session
from aitbc_chain.models import Block, Transaction, Receipt
def _insert_block(session: Session, height: int = 0) -> Block:
block = Block(
height=height,
hash=f"0x{'0'*63}{height}",
parent_hash="0x" + "0" * 64,
proposer="validator",
tx_count=0,
)
session.add(block)
session.commit()
session.refresh(block)
return block
def test_relationships(session: Session) -> None:
block = _insert_block(session, height=1)
tx = Transaction(
tx_hash="0x" + "1" * 64,
block_height=block.height,
sender="alice",
recipient="bob",
payload={"foo": "bar"},
)
receipt = Receipt(
job_id="job-1",
receipt_id="0x" + "2" * 64,
block_height=block.height,
payload={},
miner_signature={},
coordinator_attestations=[],
)
session.add(tx)
session.add(receipt)
session.commit()
session.refresh(tx)
session.refresh(receipt)
assert tx.block is not None
assert tx.block.hash == block.hash
assert receipt.block is not None
assert receipt.block.hash == block.hash
def test_hash_validation_accepts_hex(session: Session) -> None:
block = Block(
height=10,
hash="0x" + "a" * 64,
parent_hash="0x" + "b" * 64,
proposer="validator",
)
session.add(block)
session.commit()
session.refresh(block)
assert block.hash.startswith("0x")
assert block.parent_hash.startswith("0x")
def test_hash_validation_rejects_non_hex(session: Session) -> None:
with pytest.raises(ValueError):
Block(
height=20,
hash="not-hex",
parent_hash="0x" + "c" * 64,
proposer="validator",
)
with pytest.raises(ValueError):
Transaction(
tx_hash="bad",
sender="alice",
recipient="bob",
payload={},
)
with pytest.raises(ValueError):
Receipt(
job_id="job",
receipt_id="oops",
payload={},
miner_signature={},
coordinator_attestations=[],
)

View File

@ -0,0 +1,46 @@
from __future__ import annotations
import asyncio
from fastapi.testclient import TestClient
from aitbc_chain.app import create_app
from aitbc_chain.gossip import gossip_broker
def _publish(topic: str, message: dict) -> None:
asyncio.run(gossip_broker.publish(topic, message))
def test_blocks_websocket_stream() -> None:
client = TestClient(create_app())
with client.websocket_connect("/rpc/ws/blocks") as websocket:
payload = {
"height": 1,
"hash": "0x" + "1" * 64,
"parent_hash": "0x" + "0" * 64,
"timestamp": "2025-01-01T00:00:00Z",
"tx_count": 2,
}
_publish("blocks", payload)
message = websocket.receive_json()
assert message == payload
def test_transactions_websocket_stream() -> None:
client = TestClient(create_app())
with client.websocket_connect("/rpc/ws/transactions") as websocket:
payload = {
"tx_hash": "0x" + "a" * 64,
"sender": "alice",
"recipient": "bob",
"payload": {"amount": 1},
"nonce": 1,
"fee": 0,
"type": "TRANSFER",
}
_publish("transactions", payload)
message = websocket.receive_json()
assert message == payload