From d3c4f2e97e650435339af6e6fb8cc7d8ceceb1f2 Mon Sep 17 00:00:00 2001 From: btcneves Date: Sat, 9 May 2026 10:42:36 -0300 Subject: [PATCH 1/5] Fix cluster mempool version gate --- PROJECT_STATUS.md | 4 +- README.md | 6 +- README.pt-BR.md | 6 +- RELEASE_NOTES_v1.1.0.md | 4 +- ROADMAP.md | 4 +- api/service.py | 86 +++++++++++++++++++--- docs/api.md | 2 +- docs/implementation-plan-v1.2.md | 2 +- docs/presentation/README.md | 4 +- docs/presentation/evaluator-checklist.md | 2 +- docs/presentation/faq.md | 4 +- docs/presentation/pitch-3min.md | 2 +- docs/presentation/submission-text.md | 10 +-- docs/presentation/technical-walkthrough.md | 6 +- frontend/src/i18n/enUS.ts | 6 +- frontend/src/i18n/glossary.ts | 4 +- frontend/src/i18n/ptBR.ts | 6 +- tests/test_api.py | 32 +++++++- 18 files changed, 142 insertions(+), 48 deletions(-) diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 40715f6..e20d367 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -161,7 +161,7 @@ If SQLite initialisation fails, the API transparently falls back to an in-memory - The official demo uses Bitcoin Core regtest only; signet/mainnet operation is intentionally out of scope. - `logs/` is local runtime storage, not a production database. - Local non-Docker mode is available for development, but Docker is the validated judging path. -- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core v28+. BC26 returns `unavailable` (documented). +- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core v31+. BC26 returns `unavailable` (documented). - Reorg Lab is experimental and runs only in regtest. Not suitable for production. - `prometheus-client` must be installed for `/metrics` to return Prometheus data (included in `requirements.txt`). - SQLite history database is local to the container volume. History does not survive `docker compose down -v` unless the volume is preserved. @@ -196,4 +196,4 @@ If SQLite initialisation fails, the API transparently falls back to an in-memory | Multi-node support | Planned | | Kubernetes manifests / Helm chart | 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) | +| Cluster mempool visualization | Ready (fallback visual groups; BC31+ RPCs detected when available) | diff --git a/README.md b/README.md index 50eba5a..fe6b500 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ NodeScope returns an honest `unavailable` status with a clear explanation — ne 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. +> Cluster mempool RPCs are expected in Bitcoin Core 31+. This build uses Bitcoin Core 26. --- @@ -523,7 +523,7 @@ Output: latency table (min/mean/median/p95/max) per endpoint. Results vary by ho ## Limitations - **Regtest-only** for demo scenarios. Mainnet/signet/testnet observability is possible with configuration changes but not validated in this release. -- **Cluster mempool RPCs** (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 28+. This build uses Bitcoin Core 26 — these RPCs return `unavailable` with an honest explanation. +- **Cluster mempool RPCs** (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 31+. This build uses Bitcoin Core 26 — these RPCs return `unavailable` with an honest explanation. - **Reorg Lab** is marked **experimental**: the scenario is reproducible in regtest but may behave differently depending on wallet state. - **CPFP child construction** requires the parent output to be tracked in the wallet (`listunspent minconf=0`). If not found, a fallback path is used and the proof records it. - **ZMQ events** are stored as NDJSON in `logs/`. There is no persistence across container restarts. @@ -538,7 +538,7 @@ Output: latency table (min/mean/median/p95/max) per endpoint. Results vary by ho |---|---| | Signet/testnet support | 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) | +| Cluster mempool visualization | Ready (fallback visual groups; BC31+ RPCs detected when available) | | Mempool eviction scenario | Planned | | Multi-node topology | Planned | | Postgres / TimescaleDB for event persistence | Planned | diff --git a/README.pt-BR.md b/README.pt-BR.md index 777eb35..d8e937a 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -300,7 +300,7 @@ o NodeScope retorna um status `unavailable` honesto com explicação clara — n 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. +> RPCs de cluster mempool são esperados no Bitcoin Core 31+. Esta build usa Bitcoin Core 26. --- @@ -522,7 +522,7 @@ Saída: tabela de latência (min/média/mediana/p95/max) por endpoint. Os result ## Limitações - **Apenas regtest** para cenários de demo. Observabilidade em mainnet/signet/testnet é possível com mudanças de configuração, mas não validada nesta versão. -- **RPCs de cluster mempool** (`getmempoolcluster`, `getmempoolfeeratediagram`) exigem Bitcoin Core 28+. Esta build usa Bitcoin Core 26 — esses RPCs retornam `unavailable` com explicação honesta. +- **RPCs de cluster mempool** (`getmempoolcluster`, `getmempoolfeeratediagram`) exigem Bitcoin Core 31+. Esta build usa Bitcoin Core 26 — esses RPCs retornam `unavailable` com explicação honesta. - **Reorg Lab** é marcado como **experimental**: o cenário é reproduzível em regtest, mas pode ter comportamento diferente dependendo do estado da carteira. - **Construção do child CPFP** requer que o output do parent esteja rastreado na carteira (`listunspent minconf=0`). Se não encontrado, um caminho alternativo é usado e a prova o registra. - **Eventos ZMQ** são armazenados como NDJSON em `logs/`. Não há persistência entre reinicializações de container. @@ -537,7 +537,7 @@ Saída: tabela de latência (min/média/mediana/p95/max) por endpoint. Os result |---|---| | Suporte a signet/testnet | 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) | +| Visualização de cluster mempool | Pronto (grupos visuais via fallback; RPCs BC31+ detectados quando disponíveis) | | Cenário de expulsão da mempool | Planejado | | Topologia multi-nó | Planejado | | Postgres / TimescaleDB para persistência de eventos | Planejado | diff --git a/RELEASE_NOTES_v1.1.0.md b/RELEASE_NOTES_v1.1.0.md index 6873afb..09f2f9b 100644 --- a/RELEASE_NOTES_v1.1.0.md +++ b/RELEASE_NOTES_v1.1.0.md @@ -61,7 +61,7 @@ RPC offline (critical), simulation errors (warning), cluster mempool unavailable **Cluster Mempool Detector** Probes `getmempoolcluster` and `getmempoolfeeratediagram`. Returns an honest `unavailable` -on Bitcoin Core 26 — never a false positive. Ready for Bitcoin Core 28+. +on Bitcoin Core 26 — never a false positive. Ready for Bitcoin Core 31+. --- @@ -155,7 +155,7 @@ No database migrations required. SQLite history is created automatically on firs ## Known Limitations - Regtest-only for demo scenarios. Signet/mainnet support is planned. -- Cluster mempool RPCs require Bitcoin Core 28+. This release uses Bitcoin Core 26 — `getmempoolcluster` returns `unavailable`. +- Cluster mempool RPCs require Bitcoin Core 31+. This release uses Bitcoin Core 26 — `getmempoolcluster` returns `unavailable`. - Reorg Lab is marked experimental. Behavior may vary depending on wallet state. - SQLite history is local to the container volume and does not survive `docker compose down -v`. - `estimatesmartfee` returns `unavailable` or `limited` in regtest — no real fee market exists. diff --git a/ROADMAP.md b/ROADMAP.md index ac27b77..85e50e2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -32,7 +32,7 @@ Everything below is shipped and functional in the current release. | 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 | +| Visual cluster mempool view | Uses BC31+ 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 | @@ -65,7 +65,7 @@ Nothing is currently in active development. | Dashboard adapted for signet | Remove "mine block" controls; read-only mode indicators | | Mainnet read-only mode | `BITCOIN_NETWORK=mainnet` with explicit network safeguards | | 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 | +| Enhanced Bitcoin Core 31+ 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 | | OpenTelemetry traces | RPC, ZMQ, and API request traces | diff --git a/api/service.py b/api/service.py index c53b7f5..ca03769 100644 --- a/api/service.py +++ b/api/service.py @@ -754,6 +754,37 @@ def iter_live_events_sse( # Cluster Mempool Compatibility Detector # --------------------------------------------------------------------------- +_CLUSTER_MEMPOOL_MIN_VERSION = (31, 0, 0) +_CLUSTER_MEMPOOL_RPCS = ("getmempoolcluster", "getmempoolfeeratediagram") + + +def _parse_core_version(value: Any) -> tuple[int, int, int] | None: + if isinstance(value, int): + return value // 10_000, (value // 100) % 100, value % 100 + if not isinstance(value, str): + return None + cleaned = value.strip().strip("/") + if cleaned.startswith("Satoshi:"): + cleaned = cleaned.removeprefix("Satoshi:") + parts = [] + for part in cleaned.split("."): + if not part.isdigit(): + break + parts.append(int(part)) + if not parts: + return None + padded = [*parts, 0, 0] + return padded[0], padded[1], padded[2] + + +def _version_label(version_str: str | None, version_tuple: tuple[int, int, int] | None) -> str: + if version_str: + return version_str + if version_tuple: + major, minor, patch = version_tuple + return f"{major}.{minor}.{patch}" + return "this Bitcoin Core build" + def get_cluster_compatibility() -> dict: """Probe whether cluster mempool RPCs exist in the current Bitcoin Core build.""" @@ -761,21 +792,55 @@ def get_cluster_compatibility() -> dict: # Retrieve version string for display version_str: str | None = None + version_tuple: tuple[int, int, int] | None = None try: net_info = rpc.getnetworkinfo() version_str = net_info.get("subversion") or str(net_info.get("version", "")) + version_tuple = _parse_core_version(net_info.get("version")) or _parse_core_version( + version_str + ) except RPCError: pass + if version_tuple is not None and version_tuple < _CLUSTER_MEMPOOL_MIN_VERSION: + version_label = _version_label(version_str, version_tuple) + reason = ( + "Cluster mempool RPCs require Bitcoin Core 31.0 or newer; " + f"connected node is {version_label}." + ) + return { + "bitcoin_core_version": version_str, + "supported": False, + "rpcs": [ + { + "rpc": rpc_name, + "supported": False, + "reason": reason, + } + for rpc_name in _CLUSTER_MEMPOOL_RPCS + ], + "message": ( + "Cluster mempool RPCs are not available in this Bitcoin Core version. " + "NodeScope detects support automatically and uses an honest fallback when " + "unavailable. These RPCs were added in Bitcoin Core 31.0." + ), + "note": reason, + } + results = [] - for rpc_name in ("getmempoolcluster", "getmempoolfeeratediagram"): + for rpc_name in _CLUSTER_MEMPOOL_RPCS: try: - rpc.call(rpc_name) + rpc.call("help", [rpc_name]) results.append({"rpc": rpc_name, "supported": True, "reason": None}) except RPCError as exc: err = str(exc) - if "Method not found" in err or "-32601" in err or "not found" in err.lower(): + if ( + "Method not found" in err + or "-32601" in err + or "not found" in err.lower() + or "unknown command" in err.lower() + ): results.append( { "rpc": rpc_name, @@ -792,25 +857,24 @@ def get_cluster_compatibility() -> dict: } ) - any_supported = any(r["supported"] for r in results) + all_supported = all(r["supported"] for r in results) return { "bitcoin_core_version": version_str, - "supported": any_supported, + "supported": all_supported, "rpcs": results, "message": ( "Cluster mempool RPCs detected and available." - if any_supported + if all_supported else "Cluster mempool RPCs are not available in this Bitcoin Core build. " "NodeScope detects support automatically and uses an honest fallback when unavailable. " - "These RPCs are not yet included in any official Bitcoin Core release." + "These RPCs were added in Bitcoin Core 31.0." ), "note": ( None - if any_supported + if all_supported else ( - f"{version_str} does not include getmempoolcluster or getmempoolfeeratediagram." - if version_str - else "This build does not include getmempoolcluster or getmempoolfeeratediagram." + f"{_version_label(version_str, version_tuple)} does not include " + "getmempoolcluster and getmempoolfeeratediagram." ) ), } diff --git a/docs/api.md b/docs/api.md index 966ae13..ee07a33 100644 --- a/docs/api.md +++ b/docs/api.md @@ -567,7 +567,7 @@ Probes whether the connected Bitcoin Core node supports cluster mempool RPCs. { "getmempoolcluster": "unavailable", "getmempoolfeeratediagram": "unavailable", - "note": "Cluster mempool RPCs require Bitcoin Core 28+. This node runs 26.x." + "note": "Cluster mempool RPCs require Bitcoin Core 31+. This node runs 26.x." } ``` diff --git a/docs/implementation-plan-v1.2.md b/docs/implementation-plan-v1.2.md index 25bf3b1..d62fdb1 100644 --- a/docs/implementation-plan-v1.2.md +++ b/docs/implementation-plan-v1.2.md @@ -11,7 +11,7 @@ | 3 | Alertas configuráveis (CRUD dinâmico) | Pendente | | 4 | Ordenação avançada em tabelas | Pendente | | 5 | Rate limiting (sliding window) | **Arquivo criado** ✓ | -| 6 | Cluster mempool visual real (Bitcoin Core 28+) | Pendente | +| 6 | Cluster mempool visual real (Bitcoin Core 31+) | Pendente | ## Estado Atual (2026-05-08) diff --git a/docs/presentation/README.md b/docs/presentation/README.md index 4234baf..bd896b8 100644 --- a/docs/presentation/README.md +++ b/docs/presentation/README.md @@ -49,7 +49,7 @@ Open `http://localhost:5173` → click "Run Full Demo" in Guided Demo. | Python unit tests | 80 | | Prometheus metrics | 28+ | | i18n | PT-BR / EN-US | -| Cluster mempool (BC28+ RPCs) | Unavailable on BC26 (documented) | +| Cluster mempool (BC31+ RPCs) | Unavailable on BC26 (documented) | | Reorg Lab | Ready (experimental) | --- @@ -69,7 +69,7 @@ Open `http://localhost:5173` → click "Run Full Demo" in Guided Demo. ## Honest Limitations - Regtest only for all demo scenarios. -- Cluster mempool RPCs require Bitcoin Core 28+. +- Cluster mempool RPCs require Bitcoin Core 31+. - Reorg Lab is experimental. - `estimatesmartfee` in regtest may return null for some targets. - SQLite history is local to the container volume. diff --git a/docs/presentation/evaluator-checklist.md b/docs/presentation/evaluator-checklist.md index 9261eca..f9bfba7 100644 --- a/docs/presentation/evaluator-checklist.md +++ b/docs/presentation/evaluator-checklist.md @@ -133,7 +133,7 @@ Complete this checklist to fully assess NodeScope in under 10 minutes. - [ ] Policy Arena proof records the real RPC call results - [ ] `/metrics` shows `nodescope_rpc_up 1.0` - [ ] No secrets, tokens, or private keys visible in the browser or logs -- [ ] Cluster Mempool Detector shows `unavailable` for BC28+ RPCs (expected on BC26) +- [ ] Cluster Mempool Detector shows `unavailable` for BC31+ RPCs (expected on BC26) - [ ] Reorg Lab is marked experimental - [ ] Fee Estimation shows regtest limitations honestly diff --git a/docs/presentation/faq.md b/docs/presentation/faq.md index 511c475..546daaa 100644 --- a/docs/presentation/faq.md +++ b/docs/presentation/faq.md @@ -135,7 +135,7 @@ No. The entire demo stack is self-contained: ## What are the limitations? - **Regtest only**: All demo scenarios use Bitcoin Core regtest. No mainnet or signet validation. -- **Cluster mempool**: `getmempoolcluster` and `getmempoolfeeratediagram` require Bitcoin Core 28+. BC26 (used here) does not support them — the UI shows this clearly. +- **Cluster mempool**: `getmempoolcluster` and `getmempoolfeeratediagram` require Bitcoin Core 31+. BC26 (used here) does not support them — the UI shows this clearly. - **Reorg Lab**: Experimental. Results depend on wallet state. - **Fee Estimation**: `estimatesmartfee` in regtest may return null fees for some targets due to limited fee history. - **History**: SQLite is local to the container volume and does not survive `docker compose down -v`. @@ -147,7 +147,7 @@ No. The entire demo stack is self-contained: | Feature | Status | |---|---| -| Cluster mempool visualization (Bitcoin Core 28+) | Planned | +| Cluster mempool visualization (Bitcoin Core 31+) | Planned | | Signet / testnet read-only mode | Planned | | OpenTelemetry traces (RPC, ZMQ, API) | Planned | | Postgres / TimescaleDB for event persistence | Planned | diff --git a/docs/presentation/pitch-3min.md b/docs/presentation/pitch-3min.md index c4af74e..a479057 100644 --- a/docs/presentation/pitch-3min.md +++ b/docs/presentation/pitch-3min.md @@ -135,7 +135,7 @@ SQLite-backed persistence stores proof reports, demo runs, policy runs, and reor ### 12. Security and Limitations (15s) - Demo uses Bitcoin Core regtest only. No mainnet. No real money. -- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 28+. BC26 returns `unavailable` with an honest explanation. +- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 31+. BC26 returns `unavailable` with an honest explanation. - Reorg Lab is experimental. - ZMQ is treated as a notification source; RPC is used for final validation. - No private keys, seeds, or wallet credentials in the repository. diff --git a/docs/presentation/submission-text.md b/docs/presentation/submission-text.md index b743a00..f974ca4 100644 --- a/docs/presentation/submission-text.md +++ b/docs/presentation/submission-text.md @@ -62,7 +62,7 @@ NodeScope provides that layer: a real-time dashboard backed by direct Bitcoin Co 1. **Real Bitcoin Core, real RPC, real ZMQ** — not a simulator, not a mock. 2. **Guided, visual, auditable** — evaluators see and reproduce results without terminal expertise. -3. **Honest engineering** — all limitations are documented inline (cluster mempool requires BC28+, regtest fee estimation caveats, experimental reorg). +3. **Honest engineering** — all limitations are documented inline (cluster mempool requires BC31+, regtest fee estimation caveats, experimental reorg). 4. **Proof reports** — every demo and scenario generates a verifiable JSON artifact. 5. **Professional observability** — Prometheus metrics, operational alerting, reproducible benchmark. 6. **Bilingual** — PT-BR / EN-US throughout. @@ -85,14 +85,14 @@ Full evaluator guide: [docs/presentation/evaluator-checklist.md](evaluator-check ### Limitations (Honest) - All demo scenarios use Bitcoin Core regtest. No mainnet, no real value. -- Cluster mempool RPCs require Bitcoin Core 28+. BC26 (used here) returns `unavailable`. +- Cluster mempool RPCs require Bitcoin Core 31+. BC26 (used here) returns `unavailable`. - Reorg Lab is experimental — reproducible in regtest, behavior depends on wallet state. - `estimatesmartfee` in regtest has limited historical data; some targets may return null fees. - SQLite history is local to the container volume. ### Next Steps -- Cluster mempool visualization (Bitcoin Core 28+ compatibility) +- Cluster mempool visualization (Bitcoin Core 31+ compatibility) - Signet / testnet read-only observation mode - OpenTelemetry traces for RPC, ZMQ, and API calls - Postgres / TimescaleDB for scalable event persistence @@ -160,14 +160,14 @@ Guia completo: [docs/presentation/evaluator-checklist.md](evaluator-checklist.md ### Limitações (Honestas) - Todos os cenários de demo usam Bitcoin Core regtest. Sem mainnet, sem valor real. -- RPCs de cluster mempool exigem Bitcoin Core 28+. BC26 retorna `unavailable`. +- RPCs de cluster mempool exigem Bitcoin Core 31+. BC26 retorna `unavailable`. - Reorg Lab é experimental. - `estimatesmartfee` em regtest tem histórico limitado; alguns alvos podem retornar taxas nulas. - Histórico SQLite é local ao volume do container. ### Próximos Passos -- Visualização de cluster mempool (compatibilidade com BC28+) +- Visualização de cluster mempool (compatibilidade com BC31+) - Modo de observação signet/testnet read-only - Traces OpenTelemetry para RPC, ZMQ e API - Postgres/TimescaleDB para persistência escalável de eventos diff --git a/docs/presentation/technical-walkthrough.md b/docs/presentation/technical-walkthrough.md index 4848fbf..d8e05be 100644 --- a/docs/presentation/technical-walkthrough.md +++ b/docs/presentation/technical-walkthrough.md @@ -170,7 +170,7 @@ Key views: | Reorg Lab | Controlled reorg scenario (experimental) | | Fee Estimation Playground | `estimatesmartfee` for 1/3/6/12 block targets | | Historical Dashboard | Paginated list of all past runs and proof reports | -| Cluster Mempool Detector | Compatibility check for BC28+ RPCs | +| Cluster Mempool Detector | Compatibility check for BC31+ RPCs | i18n: PT-BR / EN-US toggle persisted via `localStorage`. All views are bilingual. @@ -192,7 +192,7 @@ Explainability: Each view has an ExplainBox banner, Tooltip components on techni ## Limitations - Regtest only for all demo scenarios. Mainnet/signet operation is possible with configuration but not validated in this release. -- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 28+. BC26 returns `unavailable`. +- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 31+. BC26 returns `unavailable`. - Reorg Lab is experimental. - CPFP child construction requires the parent output to be in `listunspent minconf=0`. A fallback path exists if not found. - `estimatesmartfee` in regtest returns limited data due to insufficient fee history. Limitations are documented inline in the Fee Estimation view. @@ -225,7 +225,7 @@ python3 scripts/load_smoke.py --concurrency 5 --requests 50 | Feature | Status | |---|---| -| Cluster mempool visualization (BC28+) | Planned | +| Cluster mempool visualization (BC31+) | Planned | | Mempool eviction scenario | Planned | | OpenTelemetry traces (RPC, ZMQ, API) | Planned | | Postgres / TimescaleDB for event persistence | Planned | diff --git a/frontend/src/i18n/enUS.ts b/frontend/src/i18n/enUS.ts index c2e5f29..f7207af 100644 --- a/frontend/src/i18n/enUS.ts +++ b/frontend/src/i18n/enUS.ts @@ -413,7 +413,7 @@ export const enUS: Translations = { reorgLab: 'Simulates a controlled chain reorganization in regtest: mines blocks on one chain, then mines a longer competing chain to trigger a reorg. Observe how confirmed transactions return to the mempool.', clusterMempool: - 'Detects whether the connected Bitcoin Core node supports cluster mempool RPCs (introduced in v28+). Falls back to standard mempool data with honest disclosure when unavailable.', + 'Detects whether the connected Bitcoin Core node supports cluster mempool RPCs (introduced in v31+). Falls back to standard mempool data with honest disclosure when unavailable.', proofReport: 'Cryptographically-auditable summary of the demo execution: RPC calls, ZMQ events, TXID, block height, confirmations. Can be exported as JSON for independent verification.', }, @@ -428,7 +428,7 @@ export const enUS: Translations = { reorg: 'A chain reorganization occurs when a competing chain with more cumulative proof-of-work becomes the canonical chain. Bitcoin Core switches to the longer chain, reverting any blocks that are no longer in the main chain. Transactions from reverted blocks return to the mempool and await re-confirmation.', cluster: - 'Cluster mempool (Bitcoin Core v28+) groups related transactions into clusters and evaluates their combined fee rate for eviction and mining decisions. This improves the accuracy of fee estimation and mempool management. Earlier versions use per-transaction ancestor/descendant limits instead.', + 'Cluster mempool (Bitcoin Core v31+) groups related transactions into clusters and evaluates their combined fee rate for eviction and mining decisions. This improves the accuracy of fee estimation and mempool management. Earlier versions use per-transaction ancestor/descendant limits instead.', zmq: 'Bitcoin Core publishes internal events over ZMQ (ZeroMQ) push sockets. NodeScope subscribes to rawtx (new transactions entering the mempool) and rawblock (new blocks connected to the chain). Each event is cross-validated with RPC to confirm on-chain data.', proof: 'The Proof Report captures all verifiable data points from a demo run: RPC responses, ZMQ event timestamps, TXID, block hash, fee details, and confirmation count. It can be exported as JSON and verified independently against a Bitcoin Core node.', @@ -450,7 +450,7 @@ export const enUS: Translations = { simulationErrorDesc: 'The auto-mining simulation encountered errors. Check logs for details.', clusterUnavailable: 'Cluster mempool RPCs unavailable', clusterUnavailableDesc: - 'Bitcoin Core v28+ is required for cluster mempool RPCs. Current environment uses an earlier version.', + 'Bitcoin Core v31+ is required for cluster mempool RPCs. Current environment uses an earlier version.', reorgExperimental: 'Reorg Lab is experimental', reorgExperimentalDesc: 'Reorg Lab runs only on regtest. Results may vary. Not suitable for production use.', diff --git a/frontend/src/i18n/glossary.ts b/frontend/src/i18n/glossary.ts index 703bf47..934cb15 100644 --- a/frontend/src/i18n/glossary.ts +++ b/frontend/src/i18n/glossary.ts @@ -172,9 +172,9 @@ export const glossary: GlossaryEntry[] = [ { term: 'Cluster mempool', 'pt-BR': - 'Recurso do Bitcoin Core v28+ que agrupa transações relacionadas para melhorar decisões de fee.', + 'Recurso do Bitcoin Core v31+ que agrupa transações relacionadas para melhorar decisões de fee.', 'en-US': - 'Bitcoin Core v28+ feature that groups related transactions to improve fee-based decisions.', + 'Bitcoin Core v31+ feature that groups related transactions to improve fee-based decisions.', }, { term: 'Proof Report', diff --git a/frontend/src/i18n/ptBR.ts b/frontend/src/i18n/ptBR.ts index 4d5e865..e82efab 100644 --- a/frontend/src/i18n/ptBR.ts +++ b/frontend/src/i18n/ptBR.ts @@ -418,7 +418,7 @@ export const ptBR: Translations = { reorgLab: 'Simula uma reorganização controlada em regtest: minera blocos em uma cadeia e depois minera uma cadeia concorrente mais longa para disparar a reorg. Observe como transações confirmadas retornam à mempool.', clusterMempool: - 'Detecta se o nó Bitcoin Core conectado suporta RPCs de cluster mempool (introduzidos na v28+). Usa dados padrão da mempool com divulgação honesta quando indisponível.', + 'Detecta se o nó Bitcoin Core conectado suporta RPCs de cluster mempool (introduzidos na v31+). Usa dados padrão da mempool com divulgação honesta quando indisponível.', proofReport: 'Resumo auditável da execução da demo: chamadas RPC, eventos ZMQ, TXID, altura do bloco, confirmações. Pode ser exportado como JSON para verificação independente.', }, @@ -433,7 +433,7 @@ export const ptBR: Translations = { reorg: 'Uma reorganização de cadeia ocorre quando uma cadeia concorrente com mais prova de trabalho acumulada passa a ser a cadeia canônica. O Bitcoin Core muda para a cadeia mais longa, revertendo quaisquer blocos que não estão mais na cadeia principal. Transações de blocos revertidos retornam à mempool e aguardam reconfirmação.', cluster: - 'Cluster mempool (Bitcoin Core v28+) agrupa transações relacionadas em clusters e avalia sua taxa combinada para decisões de evicção e mineração. Isso melhora a precisão da estimativa de taxa e o gerenciamento da mempool. Versões anteriores usam limites por transação de ancestrais/descendentes.', + 'Cluster mempool (Bitcoin Core v31+) agrupa transações relacionadas em clusters e avalia sua taxa combinada para decisões de evicção e mineração. Isso melhora a precisão da estimativa de taxa e o gerenciamento da mempool. Versões anteriores usam limites por transação de ancestrais/descendentes.', zmq: 'O Bitcoin Core publica eventos internos via ZMQ (ZeroMQ) em sockets push. O NodeScope assina rawtx (novas transações entrando na mempool) e rawblock (novos blocos conectados à cadeia). Cada evento é validado cruzado com RPC para confirmar dados on-chain.', proof: 'O Relatório de Prova captura todos os dados verificáveis de uma execução da demo: respostas RPC, timestamps de eventos ZMQ, TXID, hash do bloco, detalhes de taxa e contagem de confirmações. Pode ser exportado como JSON e verificado de forma independente contra um nó Bitcoin Core.', @@ -457,7 +457,7 @@ export const ptBR: Translations = { 'A simulação de mineração automática encontrou erros. Verifique os logs para detalhes.', clusterUnavailable: 'RPCs de cluster mempool indisponíveis', clusterUnavailableDesc: - 'Bitcoin Core v28+ é necessário para RPCs de cluster mempool. O ambiente atual usa uma versão anterior.', + 'Bitcoin Core v31+ é necessário para RPCs de cluster mempool. O ambiente atual usa uma versão anterior.', reorgExperimental: 'Reorg Lab é experimental', reorgExperimentalDesc: 'O Reorg Lab funciona apenas em regtest. Os resultados podem variar. Não adequado para uso em produção.', diff --git a/tests/test_api.py b/tests/test_api.py index 7b74399..64db525 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -26,7 +26,7 @@ tx_by_id, ) from api.rpc import RPCError -from api.service import iter_live_events_sse +from api.service import get_cluster_compatibility, iter_live_events_sse from engine.snapshot import load_snapshot ROOT = Path(__file__).resolve().parents[1] @@ -104,6 +104,36 @@ def test_mempool_summary_rpc_online(self) -> None: self.assertEqual(data["size"], 5) self.assertIsNone(data["error"]) + def test_cluster_compatibility_rejects_pre_31_core(self) -> None: + with patch("api.service.get_client") as mock_get: + client = mock_get.return_value + client.getnetworkinfo.return_value = { + "subversion": "/Satoshi:28.4.0/", + "version": 280400, + } + data = get_cluster_compatibility() + + self.assertFalse(data["supported"]) + self.assertEqual(data["bitcoin_core_version"], "/Satoshi:28.4.0/") + self.assertIn("31.0", data["note"]) + self.assertTrue(all(not item["supported"] for item in data["rpcs"])) + client.call.assert_not_called() + + def test_cluster_compatibility_uses_help_probe_on_31_core(self) -> None: + with patch("api.service.get_client") as mock_get: + client = mock_get.return_value + client.getnetworkinfo.return_value = { + "subversion": "/Satoshi:31.0.0/", + "version": 310000, + } + client.call.return_value = "help text" + data = get_cluster_compatibility() + + self.assertTrue(data["supported"]) + self.assertIsNone(data["note"]) + client.call.assert_any_call("help", ["getmempoolcluster"]) + client.call.assert_any_call("help", ["getmempoolfeeratediagram"]) + def test_summary_endpoint(self) -> None: data = summary(file=str(FIXTURE_FILE)) snapshot = load_snapshot(file=FIXTURE_FILE) From 367d6fa6bd37100e633e4a9753647bb2f65b5833 Mon Sep 17 00:00:00 2001 From: btcneves Date: Sat, 9 May 2026 10:45:17 -0300 Subject: [PATCH 2/5] Clarify cluster fallback UI --- .../src/components/ClusterMempoolPanel.tsx | 35 +++++++++++++++---- frontend/src/i18n/enUS.ts | 3 ++ frontend/src/i18n/ptBR.ts | 3 ++ frontend/src/i18n/types.ts | 3 ++ 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/ClusterMempoolPanel.tsx b/frontend/src/components/ClusterMempoolPanel.tsx index 1a06a83..070fb91 100644 --- a/frontend/src/components/ClusterMempoolPanel.tsx +++ b/frontend/src/components/ClusterMempoolPanel.tsx @@ -11,6 +11,7 @@ export function ClusterMempoolPanel() { const [clusters, setClusters] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const hasNativeClusterRpc = data?.supported === true const fetchData = useCallback(async () => { setLoading(true) @@ -68,27 +69,49 @@ export function ClusterMempoolPanel() { display: 'inline-block', padding: '2px 8px', fontSize: '10px', - background: '#1f2937', - border: '1px solid #374151', + background: hasNativeClusterRpc ? '#052e16' : '#1f2937', + border: `1px solid ${hasNativeClusterRpc ? '#166534' : '#374151'}`, borderRadius: '4px', - color: '#9ca3af', + color: hasNativeClusterRpc ? '#86efac' : '#d1d5db', + marginBottom: data.supported ? '12px' : '8px', + }} + > + {t.cluster.connectedNode}: {data.bitcoin_core_version} + + )} + + {data && !data.supported && ( +
- {data.bitcoin_core_version} +
+ {t.cluster.notSupported} +
+
{data.note ?? data.message}
)}
- {clusters.cluster_count} clusters · {clusters.total_tx_count} tx + {clusters.cluster_count}{' '} + {hasNativeClusterRpc ? t.cluster.nativeClusters : t.cluster.fallbackGroups} ·{' '} + {clusters.total_tx_count} tx
{!clusters.rpc_ok && clusters.error && (
{clusters.error}
)} - {clusters.clusters.length === 0 && ( + {(clusters.clusters.length === 0 || !hasNativeClusterRpc) && (
{t.cluster.fallback}
diff --git a/frontend/src/i18n/enUS.ts b/frontend/src/i18n/enUS.ts index f7207af..c937caf 100644 --- a/frontend/src/i18n/enUS.ts +++ b/frontend/src/i18n/enUS.ts @@ -274,6 +274,9 @@ export const enUS: Translations = { supported: 'Cluster mempool RPCs detected', notSupported: 'Cluster mempool RPCs not available in this Bitcoin Core version', fallback: 'Showing fallback: standard mempool data', + fallbackGroups: 'fallback groups', + nativeClusters: 'native clusters', + connectedNode: 'connected node', checking: 'Checking compatibility…', txCount: 'Transactions', totalFee: 'Total fee', diff --git a/frontend/src/i18n/ptBR.ts b/frontend/src/i18n/ptBR.ts index e82efab..9716966 100644 --- a/frontend/src/i18n/ptBR.ts +++ b/frontend/src/i18n/ptBR.ts @@ -279,6 +279,9 @@ export const ptBR: Translations = { supported: 'RPCs de cluster mempool detectados', notSupported: 'RPCs de cluster mempool não disponíveis nesta versão do Bitcoin Core', fallback: 'Exibindo fallback: dados padrão da mempool', + fallbackGroups: 'grupos fallback', + nativeClusters: 'clusters nativos', + connectedNode: 'nó conectado', checking: 'Verificando compatibilidade…', txCount: 'Transações', totalFee: 'Taxa total', diff --git a/frontend/src/i18n/types.ts b/frontend/src/i18n/types.ts index dfa5750..8b6aa7e 100644 --- a/frontend/src/i18n/types.ts +++ b/frontend/src/i18n/types.ts @@ -262,6 +262,9 @@ export interface Translations { supported: string notSupported: string fallback: string + fallbackGroups: string + nativeClusters: string + connectedNode: string checking: string txCount: string totalFee: string From 6532d39f9dfdf8a142018cd8bd3dc3531e792f2f Mon Sep 17 00:00:00 2001 From: btcneves Date: Sat, 9 May 2026 10:50:35 -0300 Subject: [PATCH 3/5] Upgrade Docker node to Bitcoin Core 31 --- CHANGELOG.md | 2 +- PROJECT_STATUS.md | 6 +- README.md | 8 +- README.pt-BR.md | 8 +- RELEASE_NOTES_v1.1.0.md | 4 +- ROADMAP.md | 4 +- api/service.py | 127 +++++++++++++++------ docker-compose.yml | 2 +- docs/demo.md | 2 +- docs/presentation/README.md | 2 +- docs/presentation/demo-script.md | 2 +- docs/presentation/evaluator-checklist.md | 4 +- docs/presentation/faq.md | 2 +- docs/presentation/pitch-3min.md | 6 +- docs/presentation/screenshots-checklist.md | 2 +- docs/presentation/submission-text.md | 6 +- docs/presentation/technical-walkthrough.md | 6 +- tests/test_api.py | 44 ++++++- 18 files changed, 169 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b551fa..bcb2c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,7 +59,7 @@ instrumentation, and a complete bilingual interface. - `/fees/compare` shows estimates alongside fee rates from most recent demo/policy runs - **Cluster Mempool Detector** — `GET /mempool/cluster/compatibility`, `ClusterMempoolPanel.tsx` - - Probes `getmempoolcluster` and `getmempoolfeeratediagram`; reports `unavailable` on BC26 without false positives + - Probes `getmempoolcluster` and `getmempoolfeeratediagram`; reports `unavailable` on pre-31 nodes without false positives ### Added — Live Simulation diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index e20d367..907b639 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -32,7 +32,7 @@ http://localhost:5173 | Area | Status | Notes | |---|---|---| -| Bitcoin Core regtest | Ready | Compose starts Bitcoin Core 26 without mainnet, wallet keys, or external services. | +| Bitcoin Core regtest | Ready | Compose starts Bitcoin Core 31 without mainnet, wallet keys, or external services. | | RPC snapshot | Ready | FastAPI reads chain and mempool state through Bitcoin Core RPC. | | ZMQ rawtx/rawblock | Ready | Monitor subscribes to real regtest ZMQ events. | | NDJSON event logs | Ready | Monitor writes append-only logs under `logs/`. | @@ -45,7 +45,7 @@ http://localhost:5173 | ZMQ Event Tape | Ready | Live rawtx/rawblock event stream with topic filters and tx linking. | | Mempool Policy Arena | Ready | 4 runnable scenarios: normal, low-fee, RBF (bumpfee), CPFP (raw tx pipeline). | | Reorg Lab | Ready (experimental) | 10-step controlled reorg via invalidateblock/reconsiderblock in regtest. | -| Cluster Mempool Detector | Ready | Detects getmempoolcluster/getmempoolfeeratediagram availability; unavailable on BC26 (documented). | +| Cluster Mempool Detector | Ready | Detects getmempoolcluster/getmempoolfeeratediagram availability; available on BC31, with fallback for older nodes. | | Proof Reports | Ready | JSON proof exported per demo/scenario/reorg run; copiável e downloadable. | | Persistence (SQLite) | Ready | Local SQLite storage of proof reports, demo/policy/reorg run history. Memory fallback if SQLite unavailable. | | Historical Dashboard | Ready | Browser dashboard listing all past runs across Proof Reports, Demo Runs, Policy Runs, and Reorg Runs with copy-proof support. | @@ -161,7 +161,7 @@ If SQLite initialisation fails, the API transparently falls back to an in-memory - The official demo uses Bitcoin Core regtest only; signet/mainnet operation is intentionally out of scope. - `logs/` is local runtime storage, not a production database. - Local non-Docker mode is available for development, but Docker is the validated judging path. -- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core v31+. BC26 returns `unavailable` (documented). +- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core v31+. pre-31 nodes return `unavailable` (documented). - Reorg Lab is experimental and runs only in regtest. Not suitable for production. - `prometheus-client` must be installed for `/metrics` to return Prometheus data (included in `requirements.txt`). - SQLite history database is local to the container volume. History does not survive `docker compose down -v` unless the volume is preserved. diff --git a/README.md b/README.md index fe6b500..d659cd0 100644 --- a/README.md +++ b/README.md @@ -296,12 +296,12 @@ cluster mempool RPCs: - `getmempoolcluster` - `getmempoolfeeratediagram` -If supported, they are used and results are displayed. If unavailable (Bitcoin Core 26 and earlier), +If supported, they are used and results are displayed. If unavailable (Bitcoin Core versions before 31), NodeScope returns an honest `unavailable` status with a clear explanation — never a false positive. Available via `GET /mempool/cluster/compatibility`, `GET /mempool/clusters`, and the **Cluster Mempool** tab. -> Cluster mempool RPCs are expected in Bitcoin Core 31+. This build uses Bitcoin Core 26. +> Cluster mempool RPCs are expected in Bitcoin Core 31+. This build uses Bitcoin Core 31. --- @@ -501,7 +501,7 @@ The dashboard includes an **Operational Alerts** panel that polls the API every - Bitcoin Core RPC offline (critical) - Live simulation errors (warning) -- Cluster mempool RPCs unavailable (info — expected on BC26) +- Cluster mempool RPCs unavailable (info — expected on pre-31 nodes) - Reorg Lab experimental note (info) Alerts are displayed in EN-US or PT-BR according to the active language toggle. @@ -523,7 +523,7 @@ Output: latency table (min/mean/median/p95/max) per endpoint. Results vary by ho ## Limitations - **Regtest-only** for demo scenarios. Mainnet/signet/testnet observability is possible with configuration changes but not validated in this release. -- **Cluster mempool RPCs** (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 31+. This build uses Bitcoin Core 26 — these RPCs return `unavailable` with an honest explanation. +- **Cluster mempool RPCs** (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 31+. This build uses Bitcoin Core 31 — these RPCs are available when the node is running. - **Reorg Lab** is marked **experimental**: the scenario is reproducible in regtest but may behave differently depending on wallet state. - **CPFP child construction** requires the parent output to be tracked in the wallet (`listunspent minconf=0`). If not found, a fallback path is used and the proof records it. - **ZMQ events** are stored as NDJSON in `logs/`. There is no persistence across container restarts. diff --git a/README.pt-BR.md b/README.pt-BR.md index d8e937a..cf71076 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -295,12 +295,12 @@ O NodeScope verifica automaticamente se o nó Bitcoin Core conectado suporta RPC - `getmempoolcluster` - `getmempoolfeeratediagram` -Se suportados, são usados e os resultados são exibidos. Se indisponíveis (Bitcoin Core 26 e anteriores), +Se suportados, são usados e os resultados são exibidos. Se indisponíveis (versões do Bitcoin Core anteriores à 31), o NodeScope retorna um status `unavailable` honesto com explicação clara — nunca um falso positivo. 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 31+. Esta build usa Bitcoin Core 26. +> RPCs de cluster mempool são esperados no Bitcoin Core 31+. Esta build usa Bitcoin Core 31. --- @@ -500,7 +500,7 @@ O dashboard inclui um painel de **Alertas Operacionais** que verifica o estado d - Bitcoin Core RPC offline (crítico) - Erros na simulação ao vivo (aviso) -- RPCs de cluster mempool indisponíveis (info — esperado no BC26) +- RPCs de cluster mempool indisponíveis (info — esperado em nós pré-31) - Nota experimental do Reorg Lab (info) Os alertas são exibidos em PT-BR ou EN-US conforme o idioma ativo. @@ -522,7 +522,7 @@ Saída: tabela de latência (min/média/mediana/p95/max) por endpoint. Os result ## Limitações - **Apenas regtest** para cenários de demo. Observabilidade em mainnet/signet/testnet é possível com mudanças de configuração, mas não validada nesta versão. -- **RPCs de cluster mempool** (`getmempoolcluster`, `getmempoolfeeratediagram`) exigem Bitcoin Core 31+. Esta build usa Bitcoin Core 26 — esses RPCs retornam `unavailable` com explicação honesta. +- **RPCs de cluster mempool** (`getmempoolcluster`, `getmempoolfeeratediagram`) exigem Bitcoin Core 31+. Esta build usa Bitcoin Core 31 — esses RPCs ficam disponíveis quando o nó está em execução. - **Reorg Lab** é marcado como **experimental**: o cenário é reproduzível em regtest, mas pode ter comportamento diferente dependendo do estado da carteira. - **Construção do child CPFP** requer que o output do parent esteja rastreado na carteira (`listunspent minconf=0`). Se não encontrado, um caminho alternativo é usado e a prova o registra. - **Eventos ZMQ** são armazenados como NDJSON em `logs/`. Não há persistência entre reinicializações de container. diff --git a/RELEASE_NOTES_v1.1.0.md b/RELEASE_NOTES_v1.1.0.md index 09f2f9b..80e96ac 100644 --- a/RELEASE_NOTES_v1.1.0.md +++ b/RELEASE_NOTES_v1.1.0.md @@ -61,7 +61,7 @@ RPC offline (critical), simulation errors (warning), cluster mempool unavailable **Cluster Mempool Detector** Probes `getmempoolcluster` and `getmempoolfeeratediagram`. Returns an honest `unavailable` -on Bitcoin Core 26 — never a false positive. Ready for Bitcoin Core 31+. +on pre-31 Bitcoin Core nodes — never a false positive. Ready for Bitcoin Core 31+. --- @@ -155,7 +155,7 @@ No database migrations required. SQLite history is created automatically on firs ## Known Limitations - Regtest-only for demo scenarios. Signet/mainnet support is planned. -- Cluster mempool RPCs require Bitcoin Core 31+. This release uses Bitcoin Core 26 — `getmempoolcluster` returns `unavailable`. +- Cluster mempool RPCs require Bitcoin Core 31+. This release uses Bitcoin Core 31 — `getmempoolcluster` is available. - Reorg Lab is marked experimental. Behavior may vary depending on wallet state. - SQLite history is local to the container volume and does not survive `docker compose down -v`. - `estimatesmartfee` returns `unavailable` or `limited` in regtest — no real fee market exists. diff --git a/ROADMAP.md b/ROADMAP.md index 85e50e2..30b9f8b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -23,7 +23,7 @@ Everything below is shipped and functional in the current release. | ZMQ Event Tape | Live rawtx/rawblock stream with topic filters and tx linking | | Mempool Policy Arena | 4 scenarios: Normal, Low-fee, RBF (BIP125), CPFP | | Reorg Lab | 10-step controlled chain reorganization (experimental) | -| Cluster Mempool Detector | Probes getmempoolcluster/getmempoolfeeratediagram; honest unavailable on BC26 | +| Cluster Mempool Detector | Probes getmempoolcluster/getmempoolfeeratediagram; honest available on BC31 | | Proof Reports | JSON audit trail per demo/scenario/reorg run; copyable and downloadable | | 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 | @@ -87,7 +87,7 @@ Nothing is currently in active development. ## Design Principles - **Observable internals.** Every classification includes confidence signals. Every scenario generates a Proof Report. -- **Honest accounting.** Unavailable features (cluster mempool on BC26, fee estimation in regtest) are reported as `unavailable` — never hidden or faked. +- **Honest accounting.** Unavailable features (cluster mempool on pre-31 nodes, fee estimation in regtest) are reported as `unavailable` — never hidden or faked. - **Replayable data.** NDJSON logs are the durable source of truth; the engine can reprocess from scratch. - **No custodial operations.** NodeScope never signs transactions or manages private keys in production mode. - **Explicit network scoping.** regtest, signet, and mainnet will be explicitly configured and guarded. diff --git a/api/service.py b/api/service.py index ca03769..1eb36cc 100644 --- a/api/service.py +++ b/api/service.py @@ -915,9 +915,89 @@ def _fee_rate_sat_vb(entry: dict[str, Any]) -> float: return round(float(fee) * 100_000_000 / vsize, 2) +def _serialize_cluster( + cluster_id: str, + txids: list[str], + mempool: dict[str, dict[str, Any]], + total_vsize: int | None = None, +) -> dict[str, Any]: + tx_items = [] + computed_vsize = 0 + total_fee = 0.0 + for txid in txids: + entry = mempool.get(txid, {}) + vsize = int(entry.get("vsize") or entry.get("weight", 0) / 4 or 0) + fee = float(entry.get("fees", {}).get("base", entry.get("fee", 0))) + computed_vsize += vsize + total_fee += fee + tx_items.append( + { + "txid": txid, + "vsize": vsize, + "fee_btc": fee, + "fee_rate_sat_vb": _fee_rate_sat_vb(entry), + "depends": entry.get("depends") or [], + "spentby": entry.get("spentby") or [], + } + ) + + return { + "id": cluster_id, + "tx_count": len(tx_items), + "total_vsize": total_vsize if total_vsize is not None else computed_vsize, + "total_fee_btc": round(total_fee, 8), + "avg_fee_rate_sat_vb": round( + sum(tx["fee_rate_sat_vb"] for tx in tx_items) / len(tx_items), 2 + ) + if tx_items + else 0, + "txs": tx_items, + } + + +def _get_native_mempool_clusters( + rpc: Any, mempool: dict[str, dict[str, Any]] +) -> list[dict[str, Any]] | None: + try: + rpc.call("help", ["getmempoolcluster"]) + except RPCError: + return None + + clusters = [] + seen: set[frozenset[str]] = set() + for txid in sorted(mempool): + try: + native = rpc.call("getmempoolcluster", [txid]) + except RPCError: + return None + chunk_txids = [ + chunk_txid + for chunk in native.get("chunks", []) + for chunk_txid in chunk.get("txs", []) + if chunk_txid in mempool + ] + if not chunk_txids: + continue + key = frozenset(chunk_txids) + if key in seen: + continue + seen.add(key) + clusters.append( + _serialize_cluster( + f"native-cluster-{len(clusters) + 1}", + chunk_txids, + mempool, + total_vsize=int(native.get("clusterweight", 0) / 4), + ) + ) + + return clusters + + def get_cluster_mempool_visual() -> dict[str, Any]: + rpc = get_client() try: - raw = get_client().getrawmempool(verbose=True) + raw = rpc.getrawmempool(verbose=True) if not isinstance(raw, dict): raw = {} except RPCError as exc: @@ -929,41 +1009,20 @@ def get_cluster_mempool_visual() -> dict[str, Any]: "error": str(exc), } + native_clusters = _get_native_mempool_clusters(rpc, raw) + if native_clusters is not None: + native_clusters.sort(key=lambda item: (item["tx_count"], item["total_vsize"]), reverse=True) + return { + "clusters": native_clusters, + "total_tx_count": len(raw), + "cluster_count": len(native_clusters), + "rpc_ok": True, + "error": None, + } + clusters = [] for index, txids in enumerate(_build_clusters(raw), start=1): - tx_items = [] - total_vsize = 0 - total_fee = 0.0 - for txid in sorted(txids): - entry = raw[txid] - vsize = int(entry.get("vsize") or entry.get("weight", 0) / 4 or 0) - fee = float(entry.get("fees", {}).get("base", entry.get("fee", 0))) - total_vsize += vsize - total_fee += fee - tx_items.append( - { - "txid": txid, - "vsize": vsize, - "fee_btc": fee, - "fee_rate_sat_vb": _fee_rate_sat_vb(entry), - "depends": entry.get("depends") or [], - "spentby": entry.get("spentby") or [], - } - ) - clusters.append( - { - "id": f"cluster-{index}", - "tx_count": len(tx_items), - "total_vsize": total_vsize, - "total_fee_btc": round(total_fee, 8), - "avg_fee_rate_sat_vb": round( - sum(tx["fee_rate_sat_vb"] for tx in tx_items) / len(tx_items), 2 - ) - if tx_items - else 0, - "txs": tx_items, - } - ) + clusters.append(_serialize_cluster(f"fallback-group-{index}", sorted(txids), raw)) clusters.sort(key=lambda item: (item["tx_count"], item["total_vsize"]), reverse=True) return { diff --git a/docker-compose.yml b/docker-compose.yml index 460f1d5..9ffab1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ x-logging: &default-logging services: nodescope-bitcoind: - image: bitcoin/bitcoin:28 + image: bitcoin/bitcoin:31 container_name: nodescope-bitcoind restart: unless-stopped command: diff --git a/docs/demo.md b/docs/demo.md index 87a6801..03d7584 100644 --- a/docs/demo.md +++ b/docs/demo.md @@ -29,7 +29,7 @@ make docker-full-demo `docker compose up -d --build` starts: -- Bitcoin Core 26 in regtest mode +- Bitcoin Core 31 in regtest mode - ZMQ publishers for `rawtx` and `rawblock` - NodeScope monitor writing append-only NDJSON logs - FastAPI backend with REST and SSE endpoints diff --git a/docs/presentation/README.md b/docs/presentation/README.md index bd896b8..1ad959d 100644 --- a/docs/presentation/README.md +++ b/docs/presentation/README.md @@ -49,7 +49,7 @@ Open `http://localhost:5173` → click "Run Full Demo" in Guided Demo. | Python unit tests | 80 | | Prometheus metrics | 28+ | | i18n | PT-BR / EN-US | -| Cluster mempool (BC31+ RPCs) | Unavailable on BC26 (documented) | +| Cluster mempool (BC31+ RPCs) | Available on BC31; fallback on pre-31 nodes (documented) | | Reorg Lab | Ready (experimental) | --- diff --git a/docs/presentation/demo-script.md b/docs/presentation/demo-script.md index 6f5540e..181cf24 100644 --- a/docs/presentation/demo-script.md +++ b/docs/presentation/demo-script.md @@ -182,7 +182,7 @@ Show: - If the Reorg Lab step hangs: cancel and show the proof from a previous run via Historical Dashboard. - If ZMQ Event Tape shows empty: run `make docker-demo` again to generate events. - If Fee Estimation returns `null` fees: expected in regtest — document it, do not avoid it. -- Cluster Mempool Detector will always show `unavailable` on BC26 — show it honestly as a documented limitation. +- Cluster Mempool Detector will show `unavailable` on pre-31 nodes — show it honestly as a documented limitation. - All timeouts and errors are shown in the UI; do not hide failures during demo. --- diff --git a/docs/presentation/evaluator-checklist.md b/docs/presentation/evaluator-checklist.md index f9bfba7..3d311e7 100644 --- a/docs/presentation/evaluator-checklist.md +++ b/docs/presentation/evaluator-checklist.md @@ -133,7 +133,7 @@ Complete this checklist to fully assess NodeScope in under 10 minutes. - [ ] Policy Arena proof records the real RPC call results - [ ] `/metrics` shows `nodescope_rpc_up 1.0` - [ ] No secrets, tokens, or private keys visible in the browser or logs -- [ ] Cluster Mempool Detector shows `unavailable` for BC31+ RPCs (expected on BC26) +- [ ] Cluster Mempool Detector shows BC31+ RPCs as available - [ ] Reorg Lab is marked experimental - [ ] Fee Estimation shows regtest limitations honestly @@ -143,6 +143,6 @@ Complete this checklist to fully assess NodeScope in under 10 minutes. - All data is from Bitcoin Core regtest. No real Bitcoin, no mainnet, no external services. - ZMQ events are real — generated by Bitcoin Core's internal event system. -- RPC calls are real — executed against a running Bitcoin Core 26 instance. +- RPC calls are real — executed against a running Bitcoin Core 31 instance. - Proof reports are deterministic and auditable. - The entire stack starts from a single `docker compose up -d --build` command. diff --git a/docs/presentation/faq.md b/docs/presentation/faq.md index 546daaa..d4aac92 100644 --- a/docs/presentation/faq.md +++ b/docs/presentation/faq.md @@ -135,7 +135,7 @@ No. The entire demo stack is self-contained: ## What are the limitations? - **Regtest only**: All demo scenarios use Bitcoin Core regtest. No mainnet or signet validation. -- **Cluster mempool**: `getmempoolcluster` and `getmempoolfeeratediagram` require Bitcoin Core 31+. BC26 (used here) does not support them — the UI shows this clearly. +- **Cluster mempool**: `getmempoolcluster` and `getmempoolfeeratediagram` require Bitcoin Core 31+. pre-31 nodes do not support them — the UI shows this clearly. - **Reorg Lab**: Experimental. Results depend on wallet state. - **Fee Estimation**: `estimatesmartfee` in regtest may return null fees for some targets due to limited fee history. - **History**: SQLite is local to the container volume and does not survive `docker compose down -v`. diff --git a/docs/presentation/pitch-3min.md b/docs/presentation/pitch-3min.md index a479057..86549de 100644 --- a/docs/presentation/pitch-3min.md +++ b/docs/presentation/pitch-3min.md @@ -55,7 +55,7 @@ React + TypeScript + Vite ─── Dashboard (frontend/) SQLite ──────────────────── Persistent proof storage (.nodescope/) ``` -Stack: Python 3.12 + FastAPI · React 18.3.1 + TypeScript + Vite 6 · Bitcoin Core 26 · Docker Compose (4 services) +Stack: Python 3.12 + FastAPI · React 18.3.1 + TypeScript + Vite 6 · Bitcoin Core 31 · Docker Compose (4 services) --- @@ -135,7 +135,7 @@ SQLite-backed persistence stores proof reports, demo runs, policy runs, and reor ### 12. Security and Limitations (15s) - Demo uses Bitcoin Core regtest only. No mainnet. No real money. -- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 31+. BC26 returns `unavailable` with an honest explanation. +- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 31+. pre-31 nodes return `unavailable` with an honest explanation. - Reorg Lab is experimental. - ZMQ is treated as a notification source; RPC is used for final validation. - No private keys, seeds, or wallet credentials in the repository. @@ -186,7 +186,7 @@ Tudo em uma stack Docker reproduzível, sem passos manuais de terminal para aval ### 4. Arquitetura (resumida) -Stack: Python 3.12 + FastAPI · React 18.3.1 + TypeScript + Vite 6 · Bitcoin Core 26 · Docker Compose (4 serviços: bitcoind, api, monitor, frontend) +Stack: Python 3.12 + FastAPI · React 18.3.1 + TypeScript + Vite 6 · Bitcoin Core 31 · Docker Compose (4 serviços: bitcoind, api, monitor, frontend) --- diff --git a/docs/presentation/screenshots-checklist.md b/docs/presentation/screenshots-checklist.md index 00795a0..c2731ea 100644 --- a/docs/presentation/screenshots-checklist.md +++ b/docs/presentation/screenshots-checklist.md @@ -87,7 +87,7 @@ Existing screenshots are in `docs/assets/`. New captures should be added there. | Screenshot | Filename | Notes | |---|---|---| -| [ ] Cluster Mempool — unavailable message | `cluster-mempool-unavailable.png` | Shows honest "unavailable on BC26" message | +| [ ] Cluster Mempool — BC31 available message | `cluster-mempool-unavailable.png` | Shows native BC31 cluster RPC availability | --- diff --git a/docs/presentation/submission-text.md b/docs/presentation/submission-text.md index f974ca4..c5ef104 100644 --- a/docs/presentation/submission-text.md +++ b/docs/presentation/submission-text.md @@ -53,7 +53,7 @@ NodeScope provides that layer: a real-time dashboard backed by direct Bitcoin Co | Storage | SQLite (primary) + in-memory fallback | | Observability | prometheus-client | | Frontend | React 18.3.1 + TypeScript + Vite 6.0.5 | -| Bitcoin node | Bitcoin Core 26 (regtest) | +| Bitcoin node | Bitcoin Core 31 (regtest) | | Infrastructure | Docker Compose (4 services) | | i18n | PT-BR / EN-US (all views) | | CI | GitHub Actions (Python 3.12, Node 18/20/24, public-clean check) | @@ -85,7 +85,7 @@ Full evaluator guide: [docs/presentation/evaluator-checklist.md](evaluator-check ### Limitations (Honest) - All demo scenarios use Bitcoin Core regtest. No mainnet, no real value. -- Cluster mempool RPCs require Bitcoin Core 31+. BC26 (used here) returns `unavailable`. +- Cluster mempool RPCs require Bitcoin Core 31+. pre-31 nodes return `unavailable`. - Reorg Lab is experimental — reproducible in regtest, behavior depends on wallet state. - `estimatesmartfee` in regtest has limited historical data; some targets may return null fees. - SQLite history is local to the container volume. @@ -160,7 +160,7 @@ Guia completo: [docs/presentation/evaluator-checklist.md](evaluator-checklist.md ### Limitações (Honestas) - Todos os cenários de demo usam Bitcoin Core regtest. Sem mainnet, sem valor real. -- RPCs de cluster mempool exigem Bitcoin Core 31+. BC26 retorna `unavailable`. +- RPCs de cluster mempool exigem Bitcoin Core 31+. nós pré-31 retornam `unavailable`. - Reorg Lab é experimental. - `estimatesmartfee` em regtest tem histórico limitado; alguns alvos podem retornar taxas nulas. - Histórico SQLite é local ao volume do container. diff --git a/docs/presentation/technical-walkthrough.md b/docs/presentation/technical-walkthrough.md index d8e05be..f75ec12 100644 --- a/docs/presentation/technical-walkthrough.md +++ b/docs/presentation/technical-walkthrough.md @@ -10,7 +10,7 @@ NodeScope is a Bitcoin Core observability layer composed of four Docker services | Service | Container | Purpose | |---|---|---| -| `bitcoind` | `nodescope-bitcoind` | Bitcoin Core 26 in regtest mode | +| `bitcoind` | `nodescope-bitcoind` | Bitcoin Core 31 in regtest mode | | `api` | `nodescope-api` | FastAPI backend on port 8000 | | `monitor` | `nodescope-monitor` | ZMQ subscriber process | | `frontend` | `nodescope-frontend` | React + Vite dashboard on port 5173 | @@ -21,7 +21,7 @@ All services are defined in `docker-compose.yml`. No external services, APIs, or ## Bitcoin Core (regtest) -- Version: Bitcoin Core 26 +- Version: Bitcoin Core 31 - Network: regtest (deterministic, isolated, no real value) - Exposed ports: RPC 18443, ZMQ 28332 - Config: `bitcoin.conf` mounted via Docker volume @@ -192,7 +192,7 @@ Explainability: Each view has an ExplainBox banner, Tooltip components on techni ## Limitations - Regtest only for all demo scenarios. Mainnet/signet operation is possible with configuration but not validated in this release. -- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 31+. BC26 returns `unavailable`. +- Cluster mempool RPCs (`getmempoolcluster`, `getmempoolfeeratediagram`) require Bitcoin Core 31+. pre-31 nodes return `unavailable`. - Reorg Lab is experimental. - CPFP child construction requires the parent output to be in `listunspent minconf=0`. A fallback path exists if not found. - `estimatesmartfee` in regtest returns limited data due to insufficient fee history. Limitations are documented inline in the Fee Estimation view. diff --git a/tests/test_api.py b/tests/test_api.py index 64db525..0cdd8b0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -26,7 +26,7 @@ tx_by_id, ) from api.rpc import RPCError -from api.service import get_cluster_compatibility, iter_live_events_sse +from api.service import get_cluster_compatibility, get_cluster_mempool_visual, iter_live_events_sse from engine.snapshot import load_snapshot ROOT = Path(__file__).resolve().parents[1] @@ -134,6 +134,48 @@ def test_cluster_compatibility_uses_help_probe_on_31_core(self) -> None: client.call.assert_any_call("help", ["getmempoolcluster"]) client.call.assert_any_call("help", ["getmempoolfeeratediagram"]) + def test_mempool_clusters_use_native_rpc_when_available(self) -> None: + with patch("api.service.get_client") as mock_get: + client = mock_get.return_value + client.getrawmempool.return_value = { + "parent": { + "vsize": 100, + "fees": {"base": 0.00001}, + "depends": [], + "spentby": ["child"], + }, + "child": { + "vsize": 120, + "fees": {"base": 0.00003}, + "depends": ["parent"], + "spentby": [], + }, + } + client.call.side_effect = [ + "help text", + { + "clusterweight": 880, + "txcount": 2, + "chunks": [{"chunkfee": 4000, "chunkweight": 880, "txs": ["parent", "child"]}], + }, + { + "clusterweight": 880, + "txcount": 2, + "chunks": [{"chunkfee": 4000, "chunkweight": 880, "txs": ["parent", "child"]}], + }, + ] + + data = get_cluster_mempool_visual() + + self.assertTrue(data["rpc_ok"]) + self.assertEqual(data["cluster_count"], 1) + self.assertEqual(data["clusters"][0]["id"], "native-cluster-1") + self.assertEqual([tx["txid"] for tx in data["clusters"][0]["txs"]], ["parent", "child"]) + self.assertEqual(data["clusters"][0]["total_vsize"], 220) + client.call.assert_any_call("help", ["getmempoolcluster"]) + client.call.assert_any_call("getmempoolcluster", ["parent"]) + client.call.assert_any_call("getmempoolcluster", ["child"]) + def test_summary_endpoint(self) -> None: data = summary(file=str(FIXTURE_FILE)) snapshot = load_snapshot(file=FIXTURE_FILE) From 7d1371ccc01d156bd9f6a9edc050c59f3c19649e Mon Sep 17 00:00:00 2001 From: btcneves Date: Sat, 9 May 2026 10:53:06 -0300 Subject: [PATCH 4/5] Format cluster mempool panel --- frontend/src/components/ClusterMempoolPanel.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/components/ClusterMempoolPanel.tsx b/frontend/src/components/ClusterMempoolPanel.tsx index 070fb91..3dbe4c4 100644 --- a/frontend/src/components/ClusterMempoolPanel.tsx +++ b/frontend/src/components/ClusterMempoolPanel.tsx @@ -93,9 +93,7 @@ export function ClusterMempoolPanel() { marginBottom: '12px', }} > -
- {t.cluster.notSupported} -
+
{t.cluster.notSupported}
{data.note ?? data.message}
)} From 9c202c7cefd6077d873604d03faa63f1bf69a358 Mon Sep 17 00:00:00 2001 From: btcneves Date: Sat, 9 May 2026 10:59:57 -0300 Subject: [PATCH 5/5] Fix live simulation wallet balance check --- api/rpc.py | 3 + api/simulation_service.py | 121 ++++++++++++++++++++------------------ 2 files changed, 68 insertions(+), 56 deletions(-) diff --git a/api/rpc.py b/api/rpc.py index 0ba0439..71fcf62 100644 --- a/api/rpc.py +++ b/api/rpc.py @@ -78,6 +78,9 @@ def loadwallet(self, wallet_name: str) -> dict[str, Any]: def getwalletinfo(self) -> dict[str, Any]: return self.call("getwalletinfo") # type: ignore[return-value] + def getbalances(self) -> dict[str, Any]: + return self.call("getbalances") # type: ignore[return-value] + def listwallets(self) -> list[str]: return self.call("listwallets") # type: ignore[return-value] diff --git a/api/simulation_service.py b/api/simulation_service.py index 7b0140c..8471e0b 100644 --- a/api/simulation_service.py +++ b/api/simulation_service.py @@ -86,8 +86,8 @@ def _ensure_funded() -> None: miner = _wallet_rpc(_MINER_WALLET) try: - info = miner.getwalletinfo() - balance = float(info.get("balance", 0)) + balances = miner.getbalances() + balance = float(balances.get("mine", {}).get("trusted", 0)) if balance < 1.0: addr = miner.getnewaddress("sim_funding", "bech32") logger.info("simulation: mining 101 initial blocks to fund miner wallet") @@ -118,8 +118,8 @@ def _mine_block() -> None: def _send_transaction() -> None: miner = _wallet_rpc(_MINER_WALLET) try: - info = miner.getwalletinfo() - balance = float(info.get("balance", 0)) + balances = miner.getbalances() + balance = float(balances.get("mine", {}).get("trusted", 0)) if balance <= 0.01: logger.info("simulation: miner balance low (%.6f BTC), mining 5 more blocks", balance) addr = miner.getnewaddress("sim_refund", "bech32") @@ -155,61 +155,66 @@ def _send_transaction() -> None: def _run_loop() -> None: global _block_interval, _tx_interval - logger.info( - "simulation: loop started (block_interval=%ds, tx_interval=%ds)", - _block_interval, - _tx_interval, - ) - try: - _ensure_funded() - except Exception as exc: - logger.error("simulation: initial _ensure_funded failed: %s", exc) + logger.info( + "simulation: loop started (block_interval=%ds, tx_interval=%ds)", + _block_interval, + _tx_interval, + ) - last_block_time = time.time() - _block_interval # trigger immediately - last_tx_time = time.time() - _tx_interval # trigger immediately - - while not _stop_event.is_set(): - now = time.time() - - # Capture current intervals (may change via configure()) + try: + _ensure_funded() + except Exception as exc: + logger.error("simulation: initial _ensure_funded failed: %s", exc) + + last_block_time = time.time() - _block_interval # trigger immediately + last_tx_time = time.time() - _tx_interval # trigger immediately + + while not _stop_event.is_set(): + now = time.time() + + # Capture current intervals (may change via configure()) + with _lock: + bi = _block_interval + ti = _tx_interval + + # Update countdowns + nb = max(0, int(bi - (now - last_block_time))) + nt = max(0, int(ti - (now - last_tx_time))) + with _lock: + _state["next_block_in"] = nb + _state["next_tx_in"] = nt + + # Mine a block? + if now - last_block_time >= bi: + try: + _mine_block() + except Exception as exc: + logger.error("simulation: _mine_block error: %s", exc) + with _lock: + _state["errors"] += 1 + last_block_time = time.time() + + # Send a transaction? + if now - last_tx_time >= ti: + try: + _send_transaction() + except Exception as exc: + logger.error("simulation: _send_transaction error: %s", exc) + with _lock: + _state["errors"] += 1 + last_tx_time = time.time() + + _stop_event.wait(1) + except Exception as exc: + logger.exception("simulation: loop crashed: %s", exc) with _lock: - bi = _block_interval - ti = _tx_interval - - # Update countdowns - nb = max(0, int(bi - (now - last_block_time))) - nt = max(0, int(ti - (now - last_tx_time))) + _state["errors"] += 1 + finally: with _lock: - _state["next_block_in"] = nb - _state["next_tx_in"] = nt - - # Mine a block? - if now - last_block_time >= bi: - try: - _mine_block() - except Exception as exc: - logger.error("simulation: _mine_block error: %s", exc) - with _lock: - _state["errors"] += 1 - last_block_time = time.time() - - # Send a transaction? - if now - last_tx_time >= ti: - try: - _send_transaction() - except Exception as exc: - logger.error("simulation: _send_transaction error: %s", exc) - with _lock: - _state["errors"] += 1 - last_tx_time = time.time() - - _stop_event.wait(1) - - with _lock: - _state["running"] = False - _state["next_block_in"] = None - _state["next_tx_in"] = None + _state["running"] = False + _state["next_block_in"] = None + _state["next_tx_in"] = None logger.info("simulation: loop stopped") @@ -224,7 +229,7 @@ def start() -> dict[str, Any]: with _lock: readonly = _readonly_flag - already_running = bool(_state["running"]) + already_running = bool(_state["running"]) and _thread is not None and _thread.is_alive() if readonly or already_running: return get_status() @@ -277,6 +282,10 @@ def configure(block_interval: int | None = None, tx_interval: int | None = None) def get_status() -> dict[str, Any]: with _lock: + if _state["running"] and (_thread is None or not _thread.is_alive()): + _state["running"] = False + _state["next_block_in"] = None + _state["next_tx_in"] = None return { **_state, "read_only": _readonly_flag,