Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ VITE_API_PROXY_TARGET=http://127.0.0.1:8000
NODESCOPE_REQUIRE_API_KEY=false
NODESCOPE_API_KEY=

# ---------------------------------------------------------------------------
# Rate limiting
# ---------------------------------------------------------------------------
# Sliding-window protection for the API. Defaults are demo-friendly: read-heavy
# dashboard polling should not trip the limiter during normal evaluation, while
# mutating endpoints still have a tighter write budget.
RATE_LIMIT_ENABLED=true
RATE_LIMIT_RPM=600
RATE_LIMIT_WRITE_RPM=30

# ---------------------------------------------------------------------------
# Docker host ports
# ---------------------------------------------------------------------------
Expand Down
27 changes: 24 additions & 3 deletions PROJECT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

Status: Hackathon-ready
Release: v1.1.0
Local v1.2 implementation: validated
Docker quickstart: validated
Smoke test: passing (PASS=15 FAIL=0 WARN=0)
Frontend build: passing (TypeScript + Vite)
Python tests: passing (80 unit tests — see CI for current breakdown)
Python tests: passing (85 unit tests — see CI for current breakdown)
CI: passing (GitHub Actions — backend, frontend Node 18/20/24, public-clean check)

## Official Evaluator Flow
Expand Down Expand Up @@ -57,6 +58,11 @@ http://localhost:5173
| Live Simulation Engine | Ready | Auto-mines blocks and sends transactions in regtest at configurable intervals. |
| Prometheus /metrics | Ready | Prometheus-compatible metrics endpoint at GET /metrics. |
| Operational Alerting | Ready | Dashboard AlertingPanel shows RPC status, simulation errors, and environment notes. |
| Configurable Alert Rules | Ready | Alert thresholds can be listed, created, updated, deleted and evaluated through the API. |
| Historical Charts | Ready | Mempool size and minimum fee time-series endpoints feed the dashboard charts. |
| Network Read-only Guard | Ready | Non-regtest networks are detected and mutating lab endpoints are blocked unless explicitly allowed. |
| API Rate Limiting | Ready | Sliding-window middleware limits burst traffic while exempting long-lived streams and lab polling endpoints. |
| Visual Mempool Clusters | Ready | Shows package relationships when cluster RPCs are available and honest fallback groups when unavailable. |
| Reproducible Benchmark | Ready | `scripts/benchmark_nodescope.py` measures API latency for all key endpoints. |
| Optional API Key Auth | Ready | State-changing endpoints protected via `X-NodeScope-API-Key` header when `NODESCOPE_REQUIRE_API_KEY=true`. Read-only endpoints remain open. |
| Load Smoke Test | Ready | `scripts/load_smoke.py` runs concurrent requests against all read-only endpoints, reporting per-endpoint and aggregate latency and success rate. |
Expand All @@ -72,6 +78,7 @@ The `/metrics` endpoint exposes Prometheus-compatible metrics when `prometheus-c
- `nodescope_http_requests_total` — HTTP requests by method/endpoint/status
- `nodescope_http_request_duration_seconds` — request latency histogram
- `nodescope_http_errors_total` — 4xx/5xx responses
- `nodescope_rate_limited_total` — requests rejected by API rate limiting
- `nodescope_rpc_requests_total` — RPC calls to Bitcoin Core
- `nodescope_rpc_errors_total` — RPC errors
- `nodescope_rpc_latency_seconds` — RPC call latency
Expand Down Expand Up @@ -135,6 +142,20 @@ NODESCOPE_SQLITE_PATH=.nodescope/history.db

If SQLite initialisation fails, the API transparently falls back to an in-memory store and records the error in `/history/summary`. All history endpoints remain functional in either backend.

## v1.2 Operational Endpoints

| Method | Path | Description |
|---|---|---|
| GET | `/network/mode` | Detects current Bitcoin network and read-only state |
| GET | `/charts/mempool` | Mempool size time-series for historical charts |
| GET | `/charts/fees` | Minimum mempool fee time-series for historical charts |
| GET | `/alerts/config` | Lists alert rules and current threshold configuration |
| POST | `/alerts/config` | Creates a custom alert rule |
| PUT | `/alerts/config/{id}` | Updates an alert rule |
| DELETE | `/alerts/config/{id}` | Deletes an alert rule |
| GET | `/alerts/active` | Evaluates current active operational alerts |
| GET | `/mempool/clusters` | Detects cluster mempool support and returns package visualization data |

## Known Limitations

- The official demo uses Bitcoin Core regtest only; signet/mainnet operation is intentionally out of scope.
Expand Down Expand Up @@ -174,5 +195,5 @@ If SQLite initialisation fails, the API transparently falls back to an in-memory
| OpenTelemetry traces (RPC, ZMQ, API) | Planned |
| Multi-node support | Planned |
| Kubernetes manifests / Helm chart | Planned |
| signet / mainnet read-only mode | Planned |
| Cluster mempool visualization (Bitcoin Core 28+) | Planned |
| signet / mainnet read-only guard | Ready (mutating lab endpoints blocked outside regtest) |
| Cluster mempool visualization | Ready (fallback visual groups; BC28+ RPCs detected when available) |
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ cluster mempool RPCs:
If supported, they are used and results are displayed. If unavailable (Bitcoin Core 26 and earlier),
NodeScope returns an honest `unavailable` status with a clear explanation — never a false positive.

Available via `GET /mempool/cluster/compatibility` and in the **Policy Arena** tab.
Available via `GET /mempool/cluster/compatibility`, `GET /mempool/clusters`, and the **Cluster Mempool** tab.

> Cluster mempool RPCs are expected in Bitcoin Core 28+. This build uses Bitcoin Core 26.

Expand Down Expand Up @@ -520,11 +520,15 @@ Output: latency table (min/mean/median/p95/max) per endpoint. Results vary by ho
| Feature | Status |
|---|---|
| Signet/testnet support | Planned |
| Cluster mempool visualization (Bitcoin Core 28+) | Planned |
| Public-network read-only mode | Ready (network guard blocks lab mutations outside regtest) |
| Cluster mempool visualization | Ready (fallback visual groups; BC28+ RPCs detected when available) |
| Mempool eviction scenario | Planned |
| Multi-node topology | Planned |
| Postgres / TimescaleDB for event persistence | Planned |
| Historical dashboards | Ready (SQLite-backed) |
| Historical charts | Ready |
| Configurable alert thresholds | Ready |
| API rate limiting | Ready |
| API keys for mutating endpoints (optional) | Ready |
| OpenTelemetry traces | Planned |
| Kubernetes manifests / Helm chart | Planned |
Expand Down
8 changes: 6 additions & 2 deletions README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ O NodeScope verifica automaticamente se o nó Bitcoin Core conectado suporta RPC
Se suportados, são usados e os resultados são exibidos. Se indisponíveis (Bitcoin Core 26 e anteriores),
o NodeScope retorna um status `unavailable` honesto com explicação clara — nunca um falso positivo.

Disponível via `GET /mempool/cluster/compatibility` e na aba **Policy Arena**.
Disponível via `GET /mempool/cluster/compatibility`, `GET /mempool/clusters` e na aba **Cluster Mempool**.

> RPCs de cluster mempool são esperados no Bitcoin Core 28+. Esta build usa Bitcoin Core 26.

Expand Down Expand Up @@ -519,11 +519,15 @@ Saída: tabela de latência (min/média/mediana/p95/max) por endpoint. Os result
| Funcionalidade | Status |
|---|---|
| Suporte a signet/testnet | Planejado |
| Visualização de cluster mempool (Bitcoin Core 28+) | Planejado |
| Modo read-only para redes públicas | Pronto (proteção bloqueia mutações de laboratório fora de regtest) |
| Visualização de cluster mempool | Pronto (grupos visuais via fallback; RPCs BC28+ detectados quando disponíveis) |
| Cenário de expulsão da mempool | Planejado |
| Topologia multi-nó | Planejado |
| Postgres / TimescaleDB para persistência de eventos | Planejado |
| Dashboards históricos | Pronto (SQLite) |
| Gráficos históricos | Pronto |
| Limiares de alerta configuráveis | Pronto |
| Rate limiting da API | Pronto |
| API keys para endpoints mutantes (opcional) | Pronto |
| OpenTelemetry traces | Planejado |
| Kubernetes manifests / Helm chart | Planejado |
Expand Down
12 changes: 7 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ Everything below is shipped and functional in the current release.
| Live Simulation Engine | Auto-mines blocks and sends transactions at configurable intervals |
| Prometheus metrics (`/metrics`) | 24+ metrics covering HTTP, RPC, ZMQ, mempool, chain, simulation, storage |
| Operational Alerting Panel | Polls API every 15 s; surfaces RPC offline, simulation errors, env notes |
| Configurable alert thresholds | CRUD API and dashboard controls for operational alert rules |
| Historical trend charts | Mempool size and minimum fee time-series charts |
| Read-only network guard | Blocks mutating lab operations outside regtest unless explicitly allowed |
| API rate limiting | Sliding-window protection with demo-friendly defaults |
| Visual cluster mempool view | Uses BC28+ cluster RPCs when available; otherwise displays honest fallback groups |
| Optional API key auth | Mutating endpoints protected via `X-NodeScope-API-Key` when `NODESCOPE_REQUIRE_API_KEY=true` |
| SQLite persistence | Proof reports, demo/policy/reorg run history; in-memory fallback if SQLite unavailable |
| Historical Dashboard | Paginated view of all past runs across all scenario types |
Expand Down Expand Up @@ -59,13 +64,10 @@ Nothing is currently in active development.
| Signet/testnet observer mode | `BITCOIN_NETWORK=signet` flag; ZMQ + RPC without wallet or regtest operations |
| Dashboard adapted for signet | Remove "mine block" controls; read-only mode indicators |
| Mainnet read-only mode | `BITCOIN_NETWORK=mainnet` with explicit network safeguards |
| Rate limiting on `/events/stream` | Relevant for public deployments |
| Cluster mempool visualization | Requires Bitcoin Core 28+; gated on getmempoolcluster availability |
| Hosted deployment tuning | Public rate-limit profiles, reverse proxy examples, and SSE sizing |
| Enhanced Bitcoin Core 28+ cluster views | More detailed diagrams when getmempoolcluster/getmempoolfeeratediagram are available |
| Mempool eviction scenario | Demonstrate fee-based eviction from the mempool |
| Advanced classification heuristics | UTXO consolidation, batch payments, Taproot script patterns |
| Historical trend charts | Event rate, mempool depth, fee rate over time |
| Configurable alert thresholds | Mempool size, fee rate, block interval alerts |
| Grafana integration | Pre-built dashboard consuming `/metrics` |
| OpenTelemetry traces | RPC, ZMQ, and API request traces |
| Multi-node support | Monitor multiple Bitcoin Core instances simultaneously |

Expand Down
126 changes: 126 additions & 0 deletions api/alerts_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from __future__ import annotations

from typing import Any

from . import storage
from .rpc import RPCError, get_client

SUPPORTED_METRICS = {"mempool_size", "mempool_bytes", "minfee", "rpc_offline"}
SUPPORTED_OPERATORS = {"gt", "lt", "eq", "gte", "lte"}
SUPPORTED_SEVERITIES = {"info", "warning", "critical"}


def _compare(value: float, operator: str, threshold: float) -> bool:
if operator == "gt":
return value > threshold
if operator == "lt":
return value < threshold
if operator == "eq":
return value == threshold
if operator == "gte":
return value >= threshold
if operator == "lte":
return value <= threshold
return False


def _current_values() -> dict[str, float]:
try:
info = get_client().getmempoolinfo()
return {
"mempool_size": float(info.get("size", 0)),
"mempool_bytes": float(info.get("bytes", 0)),
"minfee": float(info.get("mempoolminfee", 0.0)) * 100_000,
"rpc_offline": 0.0,
}
except RPCError:
return {
"mempool_size": 0.0,
"mempool_bytes": 0.0,
"minfee": 0.0,
"rpc_offline": 1.0,
}


def _format_config(row: dict[str, Any]) -> dict[str, Any]:
return {
"id": row.get("id"),
"metric": row.get("metric"),
"operator": row.get("operator"),
"threshold": row.get("threshold"),
"severity": row.get("severity"),
"enabled": bool(row.get("enabled")),
"created_at": row.get("created_at"),
}


def list_configs() -> list[dict[str, Any]]:
storage.seed_default_alerts()
return [_format_config(row) for row in storage.list_alert_configs()]


def validate_rule(rule: dict[str, Any]) -> None:
metric = rule.get("metric")
operator = rule.get("operator")
severity = rule.get("severity", "warning")
if rule.get("threshold") is None:
raise ValueError("Missing threshold")
if metric not in SUPPORTED_METRICS:
raise ValueError(f"Unsupported metric: {metric}")
if operator not in SUPPORTED_OPERATORS:
raise ValueError(f"Unsupported operator: {operator}")
if severity not in SUPPORTED_SEVERITIES:
raise ValueError(f"Unsupported severity: {severity}")


def create_config(rule: dict[str, Any]) -> dict[str, Any] | None:
validate_rule(rule)
row_id = storage.insert_alert_config(
rule["metric"],
rule["operator"],
float(rule["threshold"]),
severity=rule.get("severity", "warning"),
enabled=bool(rule.get("enabled", True)),
)
return _format_config(storage.get_alert_config(row_id)) if row_id is not None else None


def update_config(config_id: int, values: dict[str, Any]) -> dict[str, Any] | None:
existing = storage.get_alert_config(config_id)
if existing is None:
return None
merged = {**existing, **{k: v for k, v in values.items() if v is not None}}
validate_rule(merged)
row = storage.update_alert_config(config_id, values)
return _format_config(row) if row else None


def delete_config(config_id: int) -> bool:
return storage.delete_alert_config(config_id)


def evaluate_alerts() -> list[dict[str, Any]]:
storage.seed_default_alerts()
values = _current_values()
active = []
for row in storage.list_alert_configs():
config = _format_config(row)
if not config["enabled"]:
continue
metric = str(config["metric"])
value = values.get(metric)
if value is None:
continue
if _compare(value, str(config["operator"]), float(config["threshold"])):
active.append(
{
"id": config["id"],
"metric": metric,
"operator": config["operator"],
"threshold": config["threshold"],
"severity": config["severity"],
"current_value": value,
"enabled": True,
}
)
return active
Loading
Loading