From 687cecdac7c510c859092289dc829bcd60e77ef5 Mon Sep 17 00:00:00 2001 From: Alejandro Cabeza Romero Date: Wed, 5 Nov 2025 11:52:08 +0100 Subject: [PATCH] Standardize env variables. Revamp README. --- Dockerfile | 5 +- README.md | 152 ++++++++++++++++++++++++++---- src/core/app.py | 15 +-- src/node/api/serializers/block.py | 7 +- src/node/manager/docker.py | 3 + 5 files changed, 151 insertions(+), 31 deletions(-) diff --git a/Dockerfile b/Dockerfile index d66d184..701108c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,11 +5,10 @@ COPY . /app WORKDIR /app # Environment variables -ENV NODE_COMPOSE_FILEPATH=/app/docker-compose.yml ENV PYTHONPATH=/app:/app/src ENV UV_INSTALL_DIR=/usr/local/bin -ENV NODE_API=http -ENV NODE_MANAGER=noop +ENV NBE_NODE_API=http +ENV NBE_NODE_MANAGER=noop # Package manager and dependencies # RUN apt-get update && apt-get install -y curl git diff --git a/README.md b/README.md index 7270dc2..358d550 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,141 @@ # Nomos Block Explorer -## Assumptions -There are a few assumptions made to facilitate the development of the PoC: -- One block per slot. -- If a range has been backfilled, it has been fully successfully backfilled. -- Backfilling strategy assumes there's, at most, one gap to fill. +This is a Proof of Concept (PoC) for a block explorer for the Nomos blockchain. -## TODO -- Better backfilling -- Upsert on backfill -- Change Sqlite -> Postgres -- Performance improvements on API and DB calls -- Fix assumptions, so we don't rely on them -- DbRepository interfaces -- Setup DB Migrations -- Tests -- Fix assumption of 1 block per slot -- Log colouring -- Handle reconnections: +## Features + +- Frontend (React-like SPA) + - Client-side routing with Home, Block, and Transaction pages. + - Home: Live stream of the latest Blocks and Transactions. + - Block: Details of a Block, including a list of its transactions. + - Transaction: Details of a Transaction. +- Backend (FastAPI) + - API + - REST API to query Blocks and Transactions. + - SSE API to stream live Blocks (and its transactions). + - Node Management + - Pluggable API (e.g. `fake`, `http`) to query nodes. + - Pluggable Manager (e.g. `noop`, `docker`) to manage local nodes. + - Simple backfilling mechanism to populate historical blocks. + +## Architecture + +The Nomos Block Explorer follows a three-tier architecture with a clear separation of concerns: + +### High-Level Overview + +```mermaid +graph LR; +A[Nomos
Node] -->|REST/SSE| B["Backend
(FastAPI)"] +B -->|REST/SSE| C["Frontend
(Preact)"] +B <--> D["Database
(SQLite)"] +``` + +### Components + +#### 1. Frontend (`/static`) +- **Framework**: Preact (lightweight React alternative) +- **Routing**: Client-side SPA routing +- **Architecture**: Component-based with functional components +- **Communication**: REST API calls and Server-Sent Events (SSE) for real-time updates + +#### 2. Backend (`/src`) +- **Framework**: FastAPI (Python async web framework) +- **API Layer** (`/src/api`): Serializers, REST and streaming endpoints +- **Core** (`/src/core`): Application setup, configuration, base types and mixins +- **Database Layer** (`/src/db`): Repository pattern for data access +- **Models** (`/src/models`): Domain models (Block, Transaction, Header, etc.) +- **Node Integration** (`/src/node`): + - **API**: Pluggable adapters to communicate with Nomos nodes (`fake`, `http`) and serializers + - **Manager**: Pluggable node lifecycle management (`noop`, `docker`) + +#### 3. Data Flow + +1. **Node Updates**: On startup, the backend starts listening for new blocks from the node and stores them in the database +2. **Backfilling**: After at least one block is in the database, the backend fetches historical blocks from the node and stores them +3. **Client Updates**: Frontend subscribes to SSE endpoints for real-time block and transaction updates +4. **Data Access**: All queries route through repository classes for consistent data access + +#### 4. Key Design Patterns + +- **Repository Pattern**: Abstraction layer for database operations (`BlockRepository`, `TransactionRepository`) +- **Strategy Pattern**: Pluggable Node API implementations (fake for testing, HTTP for production) +- **Adapter Pattern**: Serializers convert between Node API formats and internal domain models +- **Observer Pattern**: SSE streams for pushing real-time updates to clients + + +## Requirements + +- Python 3.14 +- UV Package Manager + +### Optional + +- Docker: To run a local node. + +## How to run + +1. Install the dependencies: + ```bash + uv sync + ``` + +2. Run the block explorer: + ```bash + python src/main.py + ``` + +- You can optionally run it via Docker with: + ```bash + docker build -t nomos-block-explorer . && docker run -p 8000:8000 nomos-block-explorer + ``` + +### Configuration + +The block explorer is configured through environment variables. The following variables are available: +```dotenv +NBE_LOG_LEVEL=DEBUG # DEBUG, INFO, WARNING, ERROR, CRITICAL + +NBE_DEBUG=true # Randomizes transactions in BlockSerializer + +NBE_NODE_MANAGER=noop # noop, docker +NBE_NODE_COMPOSE_FILEPATH=/path/to/docker-compose.yml # Only used if NODE_MANAGER=docker + +NBE_NODE_API=http # fake, http +NBE_NODE_API_HOST=localhost # Only used if NODE_API=http +NBE_NODE_API_PORT=18080 # Only used if NODE_API=http +NBE_NODE_API_TIMEOUT=60 # Only used if NODE_API=http +NBE_NODE_API_PROTOCOL=http # Only used if NODE_API=http + +NBE_HOST=0.0.0.0 # Block Explorer's listening host +NBE_PORT=8000 # Block Explorer's listening port +``` +If running the Block Explorer with Docker, these can be overridden. + +## Considerations + +This PoC makes simplifications to focus on the core features: +- Each slot has exactly one block. +- When backfilling, the block explorer will only backfill from the earliest block's slot to genesis. + +## Ideas and improvements + +- Fix aforementioned assumptions +- Backfilling + - Make requests concurrently + - Backfill all slots + - Upsert received blocks and transactions +- Database + - Update to Postgres + - Add migrations management + - Add relevant indexes to columns +- Add interfaces to database repositories: `BlockRepository` and `TransactionRepository` +- Add tests +- Colour logs by level +- Reconnections - Failures to connect to Node - Timeouts - Stream closed +- Frontend + - Add a block / transaction search bar + - Make pages work with block/transaction hash, rather than the `id` diff --git a/src/core/app.py b/src/core/app.py index 1f531c7..74a34e4 100644 --- a/src/core/app.py +++ b/src/core/app.py @@ -2,6 +2,7 @@ from asyncio import Task, gather from typing import Literal, Optional from fastapi import FastAPI +from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict from starlette.datastructures import State @@ -18,15 +19,15 @@ ENV_FILEPATH = DIR_REPO.joinpath(".env") class NBESettings(BaseSettings): model_config = SettingsConfigDict(env_file=ENV_FILEPATH, extra="ignore") - node_compose_filepath: str + node_compose_filepath: Optional[str] = Field(alias="NBE_NODE_COMPOSE_FILEPATH", default=None) - node_api: Literal["http", "fake"] - node_manager: Literal["docker", "noop"] + node_api: Literal["http", "fake"] = Field(alias="NBE_NODE_API") + node_manager: Literal["docker", "noop"] = Field(alias="NBE_NODE_MANAGER") - node_api_host: str = "127.0.0.1" - node_api_port: int = 18080 - node_api_timeout: int = 60 - node_api_protocol: str = "http" + node_api_host: str = Field(alias="NBE_NODE_API_HOST", default="127.0.0.1") + node_api_port: int = Field(alias="NBE_NODE_API_PORT", default=18080) + node_api_timeout: int = Field(alias="NBE_NODE_API_TIMEOUT", default=60) + node_api_protocol: str = Field(alias="NBE_NODE_API_PROTOCOL", default="http") class NBEState(State): diff --git a/src/node/api/serializers/block.py b/src/node/api/serializers/block.py index a0ef0ee..d4ecfc9 100644 --- a/src/node/api/serializers/block.py +++ b/src/node/api/serializers/block.py @@ -13,9 +13,8 @@ from utils.protocols import FromRandom def _should_randomize_transactions(): - is_debug = getenv("DEBUG", "False").lower() == "true" - is_debug__randomize_transactions = getenv("DEBUG__RANDOMIZE_TRANSACTIONS", "False").lower() == "true" - return is_debug and is_debug__randomize_transactions + is_debug = getenv("NBE_DEBUG", "False").lower() == "true" + return is_debug def _get_random_transactions() -> List[SignedTransactionSerializer]: @@ -34,7 +33,7 @@ class BlockSerializer(NbeSerializer, FromRandom): def model_validate_json(cls, *args, **kwargs) -> Self: self = super().model_validate_json(*args, **kwargs) if _should_randomize_transactions(): - logger.debug("DEBUG and DEBUG__RANDOMIZE_TRANSACTIONS are enabled, randomizing Block's transactions.") + logger.debug("DEBUG flag is enabled, randomizing Block's transactions.") self.transactions = _get_random_transactions() return self diff --git a/src/node/manager/docker.py b/src/node/manager/docker.py index 6e1ee0d..597d1c7 100644 --- a/src/node/manager/docker.py +++ b/src/node/manager/docker.py @@ -16,6 +16,9 @@ logger = logging.getLogger(__name__) class DockerModeManager(NodeManager): def __init__(self, settings: "NBESettings"): + if not settings.node_compose_filepath: + raise ValueError("Node compose filepath environment variable is not set.") + self.client: DockerClient = DockerClient( client_type="docker", compose_files=[settings.node_compose_filepath],