From 6d02e30d3b6d203f6799b8989daac5ce8c617251 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Thu, 21 May 2026 06:41:16 +0400 Subject: [PATCH 01/16] fix(snapshot): detect and recover validator vote snapshot inconsistencies - Add sanity check during export to warn if validators exist but validator votes are absent - Log warning about possible chainbase type-enum mismatch causing incomplete snapshot - Implement fallback during import to recover validator votes from legacy witness_vote key if validator_vote is empty - Improve snapshot integrity by handling potential silent corruption cases due to type enum shifts --- plugins/snapshot/plugin.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plugins/snapshot/plugin.cpp b/plugins/snapshot/plugin.cpp index f89b3dc81b..d4e9177caf 100644 --- a/plugins/snapshot/plugin.cpp +++ b/plugins/snapshot/plugin.cpp @@ -975,6 +975,18 @@ fc::mutable_variant_object snapshot_plugin::plugin_impl::serialize_state() { EXPORT_INDEX(account_authority_index, account_authority_object, "account_authority") EXPORT_INDEX(validator_index, validator_object, "validator") EXPORT_INDEX(validator_vote_index, validator_vote_object, "validator_vote") + // Sanity: if validators exist but votes are absent, the chainbase type enum + // likely shifted (types added/removed before validator_vote_object_type). + // This would silently corrupt the snapshot. + { + auto n_validators = state["validator"].get_array().size(); + auto n_votes = state["validator_vote"].get_array().size(); + if (n_validators > 0 && n_votes == 0) + wlog("SNAPSHOT INTEGRITY: ${v} validators but 0 validator votes — " + "validator_vote_index may be empty due to chainbase type-enum mismatch. " + "Snapshot will be INCOMPLETE.", + ("v", n_validators)); + } EXPORT_INDEX(block_summary_index, block_summary_object, "block_summary") EXPORT_INDEX(content_index, content_object, "content") EXPORT_INDEX(content_vote_index, content_vote_object, "content_vote") @@ -1560,6 +1572,14 @@ void snapshot_plugin::plugin_impl::load_snapshot(const fc::path& input_path) { if (state.contains("validator_vote")) { auto n = detail::import_validator_votes(db, state["validator_vote"].get_array()); ilog(CLOG_ORANGE "Imported ${n} validator votes" CLOG_RESET, ("n", n)); + // Defensive fallback: validator_vote was present but empty; the snapshot may have + // been produced from a chainbase DB with a type-enum mismatch (see export warning). + // If an old witness_vote key also exists with data, use it to recover. + if (n == 0 && state.contains("witness_vote")) { + auto n2 = detail::import_validator_votes(db, state["witness_vote"].get_array()); + if (n2 > 0) + ilog(CLOG_ORANGE "Imported ${n} validator votes (recovered from witness_vote)" CLOG_RESET, ("n", n2)); + } } else if (state.contains("witness_vote")) { // backward compat: old snapshots used "witness_vote" key auto n = detail::import_validator_votes(db, state["witness_vote"].get_array()); From 9cd2143f330c2009d97928255e39559496ffc790 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Thu, 21 May 2026 10:32:12 +0400 Subject: [PATCH 02/16] docs(build): remove all references to low-memory node build flag and options - Deleted all mentions of `LOW_MEMORY_NODE` from build scripts, environment variables, and documentation - Removed low-memory node build instructions and flags from Linux, macOS, and Windows build guides - Updated CMake options and environment variables to exclude low-memory settings - Simplified Docker image CMake flags by removing `LOW_MEMORY_NODE` - Cleared low-memory related config references in node setup and getting started guides - Cleaned up example config files by removing deprecated plugins and options related to low-memory builds --- @l10n/ru/docs/development/building.md | 9 --------- @l10n/ru/docs/node/building.md | 9 --------- @l10n/ru/docs/node/docker.md | 8 ++++---- @l10n/ru/docs/node/getting-started.md | 3 +-- @l10n/zh-CN/docs/development/building.md | 9 --------- @l10n/zh-CN/docs/node/building.md | 9 --------- @l10n/zh-CN/docs/node/docker.md | 8 ++++---- @l10n/zh-CN/docs/node/getting-started.md | 1 - docs/development/building.md | 9 --------- docs/node/building.md | 9 --------- docs/node/docker.md | 8 ++++---- docs/node/getting-started.md | 1 - share/vizd/config/config.ini | 3 --- share/vizd/config/config_debug.ini | 8 +------- share/vizd/config/config_debug_mongo.ini | 13 +------------ share/vizd/config/config_mongo.ini | 13 +------------ share/vizd/config/config_stock_exchange.ini | 2 +- share/vizd/config/config_testnet.ini | 8 +------- 18 files changed, 18 insertions(+), 112 deletions(-) diff --git a/@l10n/ru/docs/development/building.md b/@l10n/ru/docs/development/building.md index 7ba29470f0..cdd27517d6 100644 --- a/@l10n/ru/docs/development/building.md +++ b/@l10n/ru/docs/development/building.md @@ -26,10 +26,8 @@ chmod +x build-linux.sh ```bash ./build-linux.sh # Release-сборка (по умолчанию) -./build-linux.sh -l # LOW_MEMORY_NODE (узлы-валидаторы) ./build-linux.sh -n # Testnet-сборка ./build-linux.sh -t Debug -j4 # Debug-сборка с 4 параллельными задачами -./build-linux.sh --skip-deps # Пропустить установку зависимостей ./build-linux.sh --install # Установить в систему после сборки # Пользовательские пути к зависимостям @@ -54,7 +52,6 @@ chmod +x build-mac.sh **Параметры:** ```bash -./build-mac.sh -l # Low-memory узел ./build-mac.sh -n # Testnet ./build-mac.sh --skip-deps # Пропустить установки Homebrew ./build-mac.sh --boost-root /opt/boost_1_74_0 @@ -77,7 +74,6 @@ build-mingw.bat | Переменная | По умолчанию | Описание | |-----------|-------------|---------| | `VIZ_BUILD_TYPE` | Release | Release или Debug | -| `VIZ_LOW_MEMORY` | OFF | Включить low-memory узел | | `VIZ_BUILD_TESTNET` | OFF | Testnet-сборка | | `VIZ_FULL_STATIC` | OFF | Полностью статический бинарник | | `VIZ_CMAKE_EXTRA` | — | Дополнительные флаги CMake | @@ -100,7 +96,6 @@ build-msvc.bat |-----------|-------------|---------| | `VIZ_VS_VERSION` | "Visual Studio 17 2022" | Генератор Visual Studio | | `VIZ_BUILD_TYPE` | Release | Тип сборки | -| `VIZ_LOW_MEMORY` | OFF | Low-memory узел | | `VIZ_BUILD_TESTNET` | OFF | Testnet-сборка | **Требования:** Visual Studio 2019+ с нагрузкой "Desktop development with C++", CMake 3.16+. @@ -125,7 +120,6 @@ build-msvc.bat | Параметр | По умолчанию | Описание | |---------|-------------|---------| | `BUILD_TESTNET` | OFF | Сборка для testnet | -| `LOW_MEMORY_NODE` | OFF | Исключить неконсенсусные данные (уменьшает RAM) | | `CHAINBASE_CHECK_LOCKING` | OFF | Включить проверку блокировок (только для разработки) | | `BUILD_SHARED_LIBRARIES` | OFF | Собирать разделяемые библиотеки | | `USE_PCH` | OFF | Включить предкомпилированные заголовки (ускоряет пересборку) | @@ -140,9 +134,6 @@ build-msvc.bat # Release-сборка python3 programs/build_helpers/configure_build.py --release --src ../.. -# Debug с low-memory -python3 programs/build_helpers/configure_build.py --debug --low-memory - # Кросс-компиляция для Windows с MinGW python3 programs/build_helpers/configure_build.py --win --release diff --git a/@l10n/ru/docs/node/building.md b/@l10n/ru/docs/node/building.md index 60f278784d..3c70eceb8a 100644 --- a/@l10n/ru/docs/node/building.md +++ b/@l10n/ru/docs/node/building.md @@ -46,9 +46,6 @@ chmod +x build-linux.sh ### Основные флаги сборки ```bash -# Низкопамятный узел (для валидаторов/сид-узлов — без плагинов индексирования истории) -./build-linux.sh -l - # Сборка для тестнета ./build-linux.sh -n @@ -58,9 +55,6 @@ chmod +x build-linux.sh # Параллельные задания ./build-linux.sh -j 8 -# Пропустить установку зависимостей (уже установлены) -./build-linux.sh --skip-deps - # Пользовательские пути к Boost / OpenSSL ./build-linux.sh --boost-root /opt/boost_1_74_0 --openssl-root /opt/openssl ``` @@ -99,7 +93,6 @@ build-mingw.bat | Переменная | По умолчанию | Описание | |------------|--------------|----------| | `VIZ_BUILD_TYPE` | `Release` | `Release` или `Debug` | -| `VIZ_LOW_MEMORY` | `OFF` | `ON` для низкопамятного узла | | `VIZ_BUILD_TESTNET` | `OFF` | `ON` для сборки тестнета | | `VIZ_FULL_STATIC` | `OFF` | `ON` для полностью статического бинарного файла | @@ -124,7 +117,6 @@ build-msvc.bat | Опция | По умолчанию | Описание | |-------|--------------|----------| | `BUILD_TESTNET` | `OFF` | Включить код для тестнета | -| `LOW_MEMORY_NODE` | `OFF` | Исключить плагины истории/индексирования | | `CHAINBASE_CHECK_LOCKING` | `OFF` | Включить проверки блокировок (debug) | | `BUILD_SHARED_LIBRARIES` | `OFF` | Собрать разделяемые библиотеки | | `USE_PCH` | `OFF` | Включить предкомпилированные заголовки (ускоряет пересборку) | @@ -134,7 +126,6 @@ build-msvc.bat ```bash mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release \ - -DLOW_MEMORY_NODE=ON \ -DCMAKE_INSTALL_PREFIX=/usr/local \ .. make -j$(nproc) diff --git a/@l10n/ru/docs/node/docker.md b/@l10n/ru/docs/node/docker.md index 94ef93829d..4d51bf0eb1 100644 --- a/@l10n/ru/docs/node/docker.md +++ b/@l10n/ru/docs/node/docker.md @@ -129,10 +129,10 @@ docker build \ ### CMake-флаги для каждого образа -| Образ | `LOW_MEMORY_NODE` | `BUILD_TESTNET` | -|-------|:-----------------:|:---------------:| -| production | OFF | OFF | -| testnet | OFF | ON | +| Образ | `BUILD_TESTNET` | +|-------|:---------------:| +| production | OFF | +| testnet | ON | --- diff --git a/@l10n/ru/docs/node/getting-started.md b/@l10n/ru/docs/node/getting-started.md index 538b83ee05..2540d782c6 100644 --- a/@l10n/ru/docs/node/getting-started.md +++ b/@l10n/ru/docs/node/getting-started.md @@ -133,7 +133,7 @@ shared-file-size = 4G # Плагины (полный узел) plugin = chain p2p webserver json_rpc database_api network_broadcast_api -plugin = social_network tags follow account_history +plugin = account_history ``` Для узла-валидатора см. [Узел-валидатор](./validator-node.md). @@ -172,7 +172,6 @@ curl -s -X POST http://localhost:8090 \ | Полный узел | `config.ini` | Все плагины, публичные RPC-эндпоинты | | Валидатор | `config_witness.ini` | Производство блоков, RPC только на localhost | | Тестовая сеть | `config_testnet.ini` | Разработка и тестирование | -| Малая память | `config.ini` + флаг сборки `LOW_MEMORY_NODE` | Только консенсус, без индексов истории | --- diff --git a/@l10n/zh-CN/docs/development/building.md b/@l10n/zh-CN/docs/development/building.md index f7c1cb5125..8eb6279129 100644 --- a/@l10n/zh-CN/docs/development/building.md +++ b/@l10n/zh-CN/docs/development/building.md @@ -26,10 +26,8 @@ chmod +x build-linux.sh ```bash ./build-linux.sh # Release 构建(默认) -./build-linux.sh -l # LOW_MEMORY_NODE(验证者节点) ./build-linux.sh -n # Testnet 构建 ./build-linux.sh -t Debug -j4 # Debug 构建,4 个并行任务 -./build-linux.sh --skip-deps # 跳过依赖安装 ./build-linux.sh --install # 构建后安装到系统 # 自定义依赖路径 @@ -54,7 +52,6 @@ chmod +x build-mac.sh **选项:** ```bash -./build-mac.sh -l # 低内存节点 ./build-mac.sh -n # Testnet ./build-mac.sh --skip-deps # 跳过 Homebrew 安装 ./build-mac.sh --boost-root /opt/boost_1_74_0 @@ -77,7 +74,6 @@ build-mingw.bat | 变量 | 默认值 | 描述 | |------|--------|------| | `VIZ_BUILD_TYPE` | Release | Release 或 Debug | -| `VIZ_LOW_MEMORY` | OFF | 启用低内存节点 | | `VIZ_BUILD_TESTNET` | OFF | Testnet 构建 | | `VIZ_FULL_STATIC` | OFF | 完全静态二进制文件 | | `VIZ_CMAKE_EXTRA` | — | 附加 CMake 标志 | @@ -100,7 +96,6 @@ build-msvc.bat |------|--------|------| | `VIZ_VS_VERSION` | "Visual Studio 17 2022" | Visual Studio 生成器 | | `VIZ_BUILD_TYPE` | Release | 构建类型 | -| `VIZ_LOW_MEMORY` | OFF | 低内存节点 | | `VIZ_BUILD_TESTNET` | OFF | Testnet 构建 | **要求:** Visual Studio 2019+(带"Desktop development with C++"工作负载)、CMake 3.16+。 @@ -125,7 +120,6 @@ build-msvc.bat | 选项 | 默认值 | 描述 | |------|--------|------| | `BUILD_TESTNET` | OFF | 为 testnet 构建 | -| `LOW_MEMORY_NODE` | OFF | 排除非共识数据(减少 RAM) | | `CHAINBASE_CHECK_LOCKING` | OFF | 启用锁检查(仅用于开发) | | `BUILD_SHARED_LIBRARIES` | OFF | 构建共享库 | | `USE_PCH` | OFF | 启用预编译头文件(加速重新构建) | @@ -140,9 +134,6 @@ build-msvc.bat # Release 构建 python3 programs/build_helpers/configure_build.py --release --src ../.. -# 带低内存的 Debug -python3 programs/build_helpers/configure_build.py --debug --low-memory - # 使用 MinGW 交叉编译 Windows 版本 python3 programs/build_helpers/configure_build.py --win --release diff --git a/@l10n/zh-CN/docs/node/building.md b/@l10n/zh-CN/docs/node/building.md index 839144dc0c..f40637cbde 100644 --- a/@l10n/zh-CN/docs/node/building.md +++ b/@l10n/zh-CN/docs/node/building.md @@ -46,9 +46,6 @@ chmod +x build-linux.sh ### 常用构建标志 ```bash -# 低内存节点(验证者/种子节点 — 排除历史索引) -./build-linux.sh -l - # 测试网构建 ./build-linux.sh -n @@ -58,9 +55,6 @@ chmod +x build-linux.sh # 并行任务数 ./build-linux.sh -j 8 -# 跳过依赖安装(已安装) -./build-linux.sh --skip-deps - # 自定义 Boost / OpenSSL 路径 ./build-linux.sh --boost-root /opt/boost_1_74_0 --openssl-root /opt/openssl ``` @@ -99,7 +93,6 @@ build-mingw.bat | 变量 | 默认值 | 描述 | |------|-------|------| | `VIZ_BUILD_TYPE` | `Release` | `Release` 或 `Debug` | -| `VIZ_LOW_MEMORY` | `OFF` | `ON` 构建低内存节点 | | `VIZ_BUILD_TESTNET` | `OFF` | `ON` 用于测试网构建 | | `VIZ_FULL_STATIC` | `OFF` | `ON` 构建完全静态二进制文件 | @@ -124,7 +117,6 @@ build-msvc.bat | 选项 | 默认值 | 描述 | |------|-------|------| | `BUILD_TESTNET` | `OFF` | 启用测试网专用代码 | -| `LOW_MEMORY_NODE` | `OFF` | 排除历史/索引插件 | | `CHAINBASE_CHECK_LOCKING` | `OFF` | 启用锁断言检查(debug) | | `BUILD_SHARED_LIBRARIES` | `OFF` | 构建共享库 | | `USE_PCH` | `OFF` | 启用预编译头文件(加快重新构建) | @@ -134,7 +126,6 @@ build-msvc.bat ```bash mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release \ - -DLOW_MEMORY_NODE=ON \ -DCMAKE_INSTALL_PREFIX=/usr/local \ .. make -j$(nproc) diff --git a/@l10n/zh-CN/docs/node/docker.md b/@l10n/zh-CN/docs/node/docker.md index e8e96a14a9..57ec5dbd62 100644 --- a/@l10n/zh-CN/docs/node/docker.md +++ b/@l10n/zh-CN/docs/node/docker.md @@ -129,10 +129,10 @@ docker build \ ### 各镜像的 CMake 标志 -| 镜像 | `LOW_MEMORY_NODE` | `BUILD_TESTNET` | -|------|:-----------------:|:---------------:| -| production | OFF | OFF | -| testnet | OFF | ON | +| 镜像 | `BUILD_TESTNET` | +|------|:---------------:| +| production | OFF | +| testnet | ON | --- diff --git a/@l10n/zh-CN/docs/node/getting-started.md b/@l10n/zh-CN/docs/node/getting-started.md index e499476b82..41961058c1 100644 --- a/@l10n/zh-CN/docs/node/getting-started.md +++ b/@l10n/zh-CN/docs/node/getting-started.md @@ -172,7 +172,6 @@ curl -s -X POST http://localhost:8090 \ | 全节点 | `config.ini` | 所有插件,公共 RPC 端点 | | 验证者 | `config_witness.ini` | 区块生产,RPC 仅限本地 | | 测试网 | `config_testnet.ini` | 开发和测试 | -| 低内存 | `config.ini` + `LOW_MEMORY_NODE` 构建标志 | 仅共识,无历史索引 | --- diff --git a/docs/development/building.md b/docs/development/building.md index f25d27de5f..60d74805fb 100644 --- a/docs/development/building.md +++ b/docs/development/building.md @@ -26,10 +26,8 @@ chmod +x build-linux.sh ```bash ./build-linux.sh # Release build (default) -./build-linux.sh -l # LOW_MEMORY_NODE (validator nodes) ./build-linux.sh -n # Testnet build ./build-linux.sh -t Debug -j4 # Debug build with 4 parallel jobs -./build-linux.sh --skip-deps # Skip dependency installation ./build-linux.sh --install # Install to system after build # Custom dependency paths @@ -54,7 +52,6 @@ Requires Xcode Command Line Tools and Homebrew. The script installs: `boost`, `c **Options:** ```bash -./build-mac.sh -l # Low-memory node ./build-mac.sh -n # Testnet ./build-mac.sh --skip-deps # Skip Homebrew installs ./build-mac.sh --boost-root /opt/boost_1_74_0 @@ -77,7 +74,6 @@ build-mingw.bat | Variable | Default | Description | |----------|---------|-------------| | `VIZ_BUILD_TYPE` | Release | Release or Debug | -| `VIZ_LOW_MEMORY` | OFF | Enable low-memory node | | `VIZ_BUILD_TESTNET` | OFF | Testnet build | | `VIZ_FULL_STATIC` | OFF | Fully static binary | | `VIZ_CMAKE_EXTRA` | — | Additional CMake flags | @@ -100,7 +96,6 @@ build-msvc.bat |----------|---------|-------------| | `VIZ_VS_VERSION` | "Visual Studio 17 2022" | Visual Studio generator | | `VIZ_BUILD_TYPE` | Release | Build type | -| `VIZ_LOW_MEMORY` | OFF | Low-memory node | | `VIZ_BUILD_TESTNET` | OFF | Testnet build | **Requirements:** Visual Studio 2019+ with "Desktop development with C++" workload, CMake 3.16+. @@ -125,7 +120,6 @@ All Dockerfiles use a two-stage build to minimize image size and use Boost 1.71 | Option | Default | Description | |--------|---------|-------------| | `BUILD_TESTNET` | OFF | Build for testnet | -| `LOW_MEMORY_NODE` | OFF | Exclude non-consensus data (reduces RAM) | | `CHAINBASE_CHECK_LOCKING` | OFF | Enable lock checking (development only) | | `BUILD_SHARED_LIBRARIES` | OFF | Build shared libraries | | `USE_PCH` | OFF | Enable precompiled headers (faster rebuilds) | @@ -140,9 +134,6 @@ Wraps CMake with sensible defaults and cross-compilation support: # Release build python3 programs/build_helpers/configure_build.py --release --src ../.. -# Debug with low-memory -python3 programs/build_helpers/configure_build.py --debug --low-memory - # Cross-compile for Windows with MinGW python3 programs/build_helpers/configure_build.py --win --release diff --git a/docs/node/building.md b/docs/node/building.md index 682aff4ed2..f7887aa811 100644 --- a/docs/node/building.md +++ b/docs/node/building.md @@ -46,9 +46,6 @@ Output binary: `build/programs/vizd/vizd` ### Common build flags ```bash -# Low-memory node (validators/seed nodes — excludes history indexing) -./build-linux.sh -l - # Testnet build ./build-linux.sh -n @@ -58,9 +55,6 @@ Output binary: `build/programs/vizd/vizd` # Parallel jobs ./build-linux.sh -j 8 -# Skip dependency installation (already installed) -./build-linux.sh --skip-deps - # Custom Boost / OpenSSL paths ./build-linux.sh --boost-root /opt/boost_1_74_0 --openssl-root /opt/openssl ``` @@ -99,7 +93,6 @@ Optional environment variables: | Variable | Default | Description | |----------|---------|-------------| | `VIZ_BUILD_TYPE` | `Release` | `Release` or `Debug` | -| `VIZ_LOW_MEMORY` | `OFF` | `ON` to build low-memory node | | `VIZ_BUILD_TESTNET` | `OFF` | `ON` for testnet build | | `VIZ_FULL_STATIC` | `OFF` | `ON` for fully static binary | @@ -124,7 +117,6 @@ For direct CMake usage (advanced): | Option | Default | Description | |--------|---------|-------------| | `BUILD_TESTNET` | `OFF` | Enable testnet-specific code | -| `LOW_MEMORY_NODE` | `OFF` | Exclude history/indexing plugins | | `CHAINBASE_CHECK_LOCKING` | `OFF` | Enable lock assertion checks (debug) | | `BUILD_SHARED_LIBRARIES` | `OFF` | Build shared libraries | | `USE_PCH` | `OFF` | Enable precompiled headers (faster rebuilds) | @@ -134,7 +126,6 @@ Example: ```bash mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release \ - -DLOW_MEMORY_NODE=ON \ -DCMAKE_INSTALL_PREFIX=/usr/local \ .. make -j$(nproc) diff --git a/docs/node/docker.md b/docs/node/docker.md index 3205a19072..014ae11859 100644 --- a/docs/node/docker.md +++ b/docs/node/docker.md @@ -129,10 +129,10 @@ docker build \ ### CMake flags per image -| Image | `LOW_MEMORY_NODE` | `BUILD_TESTNET` | -|-------|:-----------------:|:---------------:| -| production | OFF | OFF | -| testnet | OFF | ON | +| Image | `BUILD_TESTNET` | +|-------|:---------------:| +| production | OFF | +| testnet | ON | --- diff --git a/docs/node/getting-started.md b/docs/node/getting-started.md index b7dae55713..5c8864e78b 100644 --- a/docs/node/getting-started.md +++ b/docs/node/getting-started.md @@ -171,7 +171,6 @@ Check `head_block_number` — it should increase every 3 seconds once synced. | Full node | `config.ini` | All plugins, public RPC endpoints | | Validator | `config_witness.ini` | Block production, RPC on localhost only | | Testnet | `config_testnet.ini` | Development and testing | -| Low-memory | `config.ini` + `LOW_MEMORY_NODE` build flag | Consensus only, no history indexes | --- diff --git a/share/vizd/config/config.ini b/share/vizd/config/config.ini index c29dd224b4..32139779ce 100644 --- a/share/vizd/config/config.ini +++ b/share/vizd/config/config.ini @@ -118,9 +118,6 @@ history-count-blocks = 57600 # Defines starting block from which recording stats by the account_history and operation_history plugin. history-start-block = 70000000 -# Set the maximum size of cached feed for an account -follow-max-feed-size = 500 - # name of validator controlled by this node (e.g. initwitness ) # validator = # # validator = # DEPRECATED: use 'validator' diff --git a/share/vizd/config/config_debug.ini b/share/vizd/config/config_debug.ini index 25c31e30de..7d26c2f992 100644 --- a/share/vizd/config/config_debug.ini +++ b/share/vizd/config/config_debug.ini @@ -73,7 +73,7 @@ inc-shared-file-size = 100M # and resizes. The optimal strategy is do checking of the free space, but not very often. block-num-check-free-size = 10 # each 30 seconds -plugin = chain p2p json_rpc webserver network_broadcast_api validator test_api database_api private_message follow social_network tags account_by_key account_history operation_history block_info raw_block debug_node witness_api +plugin = chain p2p json_rpc webserver network_broadcast_api validator test_api database_api account_by_key account_history operation_history block_info raw_block debug_node # Remove votes before defined block, should increase performance clear-votes-before-block = 0 # don't clear votes @@ -93,12 +93,6 @@ skip-virtual-ops = false # Defines starting block from which recording stats by the account_history plugin. # history-start-block = -# Set the maximum size of cached feed for an account -follow-max-feed-size = 500 - -# Defines a range of accounts to private messages to/from as a json pair ["from","to"] [from,to) -# pm-account-range = - # Enable block production, even if the chain is stale. enable-stale-production = true diff --git a/share/vizd/config/config_debug_mongo.ini b/share/vizd/config/config_debug_mongo.ini index 7377448471..ace73667c7 100644 --- a/share/vizd/config/config_debug_mongo.ini +++ b/share/vizd/config/config_debug_mongo.ini @@ -73,7 +73,7 @@ inc-shared-file-size = 100M # and resizes. The optimal strategy is do checking of the free space, but not very often. block-num-check-free-size = 10 # each 30 seconds -plugin = chain p2p json_rpc webserver network_broadcast_api validator test_api database_api private_message follow social_network tags market_history account_by_key account_history operation_history block_info raw_block debug_node validator_api mongo_db +plugin = chain p2p json_rpc webserver network_broadcast_api validator test_api database_api account_by_key account_history operation_history block_info raw_block debug_node validator_api mongo_db # For connect to mongodb which is running outside Docker (if vizd running inside) mongodb-uri = mongodb://172.17.0.1:27017/viz @@ -96,17 +96,6 @@ skip-virtual-ops = false # Defines starting block from which recording stats by the account_history plugin. # history-start-block = -# Set the maximum size of cached feed for an account -follow-max-feed-size = 500 - -# Track market history by grouping orders into buckets of equal size measured in seconds specified as a JSON array of numbers -bucket-size = [15,60,300,3600,86400] - -# How far back in time to track history for each bucket size, measured in the number of buckets (default: 5760) -history-per-size = 5760 - -# Defines a range of accounts to private messages to/from as a json pair ["from","to"] [from,to) -# pm-account-range = # Enable block production, even if the chain is stale. enable-stale-production = true diff --git a/share/vizd/config/config_mongo.ini b/share/vizd/config/config_mongo.ini index a8ee8be8e6..8aae5f136d 100644 --- a/share/vizd/config/config_mongo.ini +++ b/share/vizd/config/config_mongo.ini @@ -73,7 +73,7 @@ inc-shared-file-size = 2G # and resizes. The optimal strategy is do checking of the free space, but not very often. block-num-check-free-size = 1000 # each 3000 seconds -plugin = chain p2p json_rpc webserver network_broadcast_api validator test_api database_api private_message follow social_network tags market_history account_by_key operation_history account_history block_info raw_block validator_api mongo_db +plugin = chain p2p json_rpc webserver network_broadcast_api validator test_api database_api account_by_key operation_history account_history block_info raw_block validator_api mongo_db # For connect to mongodb which is running outside Docker (if vizd running inside) mongodb-uri = mongodb://172.17.0.1:27017/viz @@ -96,17 +96,6 @@ skip-virtual-ops = false # Defines starting block from which recording stats by the account_history plugin. # history-start-block = 0 -# Set the maximum size of cached feed for an account -follow-max-feed-size = 500 - -# Track market history by grouping orders into buckets of equal size measured in seconds specified as a JSON array of numbers -bucket-size = [15,60,300,3600,86400] - -# How far back in time to track history for each bucket size, measured in the number of buckets (default: 5760) -history-per-size = 5760 - -# Defines a range of accounts to private messages to/from as a json pair ["from","to"] [from,to) -# pm-account-range = # Enable block production, even if the chain is stale. enable-stale-production = false diff --git a/share/vizd/config/config_stock_exchange.ini b/share/vizd/config/config_stock_exchange.ini index 3a89b81b94..48a413df51 100644 --- a/share/vizd/config/config_stock_exchange.ini +++ b/share/vizd/config/config_stock_exchange.ini @@ -73,7 +73,7 @@ inc-shared-file-size = 2G # and resizes. The optimal strategy is do checking of the free space, but not very often. block-num-check-free-size = 1000 # each 3000 seconds -plugin = chain p2p json_rpc webserver network_broadcast_api validator database_api block_info raw_block operation_history account_history witness_api +plugin = chain p2p json_rpc webserver network_broadcast_api validator database_api block_info raw_block operation_history account_history # Remove votes before defined block, should increase performance clear-votes-before-block = 0 # clear votes after each cashout diff --git a/share/vizd/config/config_testnet.ini b/share/vizd/config/config_testnet.ini index 7281d042b8..83e1ebaa6d 100644 --- a/share/vizd/config/config_testnet.ini +++ b/share/vizd/config/config_testnet.ini @@ -73,7 +73,7 @@ inc-shared-file-size = 2G # and resizes. The optimal strategy is do checking of the free space, but not very often. block-num-check-free-size = 1000 # each 3000 seconds -plugin = validator witness_api +plugin = validator plugin = chain p2p json_rpc webserver network_broadcast_api database_api plugin = account_history operation_history plugin = committee_api invite_api paid_subscription_api custom_protocol_api @@ -97,12 +97,6 @@ skip-virtual-ops = false # Defines starting block from which recording stats by the account_history plugin. # history-start-block = 0 -# Set the maximum size of cached feed for an account -follow-max-feed-size = 500 - -# Defines a range of accounts to private messages to/from as a json pair ["from","to"] [from,to) -# pm-account-range = - # Enable block production, even if the chain is stale. enable-stale-production = true From 74ff904d180bdc65f1c871cc31bbc0cfdcfedf6b Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Thu, 21 May 2026 10:35:37 +0400 Subject: [PATCH 03/16] chore(config): remove deprecated mongo config files - Delete config_debug_mongo.ini to clean up obsolete debug mongo configuration - Remove config_mongo.ini to eliminate outdated mongo production configuration - Simplify project configuration by removing unused or legacy mongo ini files --- share/vizd/config/config_debug_mongo.ini | 132 ----------------------- share/vizd/config/config_mongo.ini | 132 ----------------------- 2 files changed, 264 deletions(-) delete mode 100644 share/vizd/config/config_debug_mongo.ini delete mode 100644 share/vizd/config/config_mongo.ini diff --git a/share/vizd/config/config_debug_mongo.ini b/share/vizd/config/config_debug_mongo.ini deleted file mode 100644 index ace73667c7..0000000000 --- a/share/vizd/config/config_debug_mongo.ini +++ /dev/null @@ -1,132 +0,0 @@ -# Endpoint for P2P node to listen on -# p2p-endpoint = - -# Maxmimum number of incoming connections on P2P endpoint -# p2p-max-connections = - -# P2P nodes to connect to on startup (may specify multiple times) -# p2p-seed-node = - -# Enable stale sync detection: when no blocks are received for the configured timeout, -# the node resets sync from the last irreversible block and reconnects all seed peers. -# p2p-stale-sync-detection = false - -# Timeout in seconds before stale sync detection triggers recovery (default: 120 = 2 minutes). -# p2p-stale-sync-timeout-seconds = 120 - -# Pairs of [BLOCK_NUM,BLOCK_ID] that should be enforced as checkpoints. -# checkpoint = - -# Number of threads for rpc-clients. Optimal value `-1` -webserver-thread-pool-size = 2 - -# IP:PORT for HTTP connections -webserver-http-endpoint = 0.0.0.0:8090 - -# IP:PORT for WebSocket connections -webserver-ws-endpoint = 0.0.0.0:8091 - -# Maximum microseconds for trying to get read lock -read-wait-micro = 500000 - -# Maximum retries to get read lock. Each retry is read-wait-micro microseconds. -# When all retries are made, the rpc-client receives error 'Unable to acquire READ lock'. -max-read-wait-retries = 2 - -# Maximum microseconds for trying to get write lock on broadcast transaction. -write-wait-micro = 500000 - -# Maximum retries to get write lock. Each retry is write-wait-micro microseconds. -# When all retries are made, the rpc-client receives error 'Unable to acquire WRITE lock'. -max-write-wait-retries = 3 - -# Do all write operations (push_block/push_transaction) in the single thread. -# Write lock of database is very heavy. When many threads tries to lock database on writing, rpc-clients -# receive many errors 'Unable to acquire READ lock' ('Unable to acquire WRITE lock'). -# Enabling of this options can increase performance. -single-write-thread = true - -# Enable plugin notifications about operations in a pushed transaction, which should be included to the next generated -# block. Plugins doesn't validate data in operations, they only update its own indexes, so notifications can be -# disabled on push_transaction() without any side-effects. The option doesn't have effect on a pushing signed blocks, -# so it is safe. -# Disabling of this option can increase performance. -enable-plugins-on-push-transaction = true - -# A start size for shared memory file when it doesn't have any data. Possible cases: -# - If shared memory has data and the value is greater then the size of shared_memory.bin, -# the file will be grown to requested size. -# - If shared memory has data and the value is less then the size of shared_memory.bin, nothing happens. -# Changing of this parameter doesn't require the replaying. -shared-file-size = 100M - -# The minimum free space in the shared memory file. When free space reaches the following value, the size of the -# shared_memory.bin increases by the value of inc-shared-file-size. -min-free-shared-file-size = 50M - -# Step of increasing size of shared_memory.bin. When the free memory size reaches min-free-shared-file-size, -# the shared memory size increases by the following value. -inc-shared-file-size = 100M - -# How often do checking the free space in shared_memory.bin. A very frequent checking can decrease performance. -# It's not critical if the free size became very small, because the daemon catches the `bad_alloc` exception -# and resizes. The optimal strategy is do checking of the free space, but not very often. -block-num-check-free-size = 10 # each 30 seconds - -plugin = chain p2p json_rpc webserver network_broadcast_api validator test_api database_api account_by_key account_history operation_history block_info raw_block debug_node validator_api mongo_db - -# For connect to mongodb which is running outside Docker (if vizd running inside) -mongodb-uri = mongodb://172.17.0.1:27017/viz - -# Remove votes before defined block, should increase performance -clear-votes-before-block = 0 # don't clear votes - -# Virtual operations will not be passed to the plugins, enabling of the option helps to save some memory. -skip-virtual-ops = false - -# Defines a range of accounts to track by the account_history plugin as a json pair ["from","to"] [from,to] -# track-account-range = - -# Defines a list of operations which will be explicitly logged by the account_history plugin. -# history-whitelist-ops = - -# Defines a list of operations which will be explicitly ignored by the account_history plugin. -# history-blacklist-ops = - -# Defines starting block from which recording stats by the account_history plugin. -# history-start-block = - - -# Enable block production, even if the chain is stale. -enable-stale-production = true - - -# Percent of validators (0-99) that must be participating in order to produce blocks -required-participation = 0 - -# name of validator controlled by this node (e.g. initwitness ) -validator = "viz" -# validator = "viz" # DEPRECATED: use 'validator' - -# WIF PRIVATE KEY to be used by one or more validators -private-key = 5JVFFWRLwz6JoP9kguuRFfytToGU6cLgBVTL9t6NB3D3BQLbUBS - -# declare an appender named "stderr" that writes messages to the console -[log.console_appender.stderr] -stream=std_error - -# declare an appender named "p2p" that writes messages to p2p.log -[log.file_appender.p2p] -filename=logs/p2p/p2p.log -# filename can be absolute or relative to this config file - -# route any messages logged to the default logger to the "stderr" logger we -# declared above, if they are info level are higher -[logger.default] -level=info -appenders=stderr - -# route messages sent to the "p2p" logger to stderr too -[logger.p2p] -level=info -appenders=stderr diff --git a/share/vizd/config/config_mongo.ini b/share/vizd/config/config_mongo.ini deleted file mode 100644 index 8aae5f136d..0000000000 --- a/share/vizd/config/config_mongo.ini +++ /dev/null @@ -1,132 +0,0 @@ -# Endpoint for P2P node to listen on -p2p-endpoint = 0.0.0.0:4243 - -# Maxmimum number of incoming connections on P2P endpoint -# p2p-max-connections = - -# P2P nodes to connect to on startup (may specify multiple times) -# p2p-seed-node = - -# Enable stale sync detection: when no blocks are received for the configured timeout, -# the node resets sync from the last irreversible block and reconnects all seed peers. -# p2p-stale-sync-detection = false - -# Timeout in seconds before stale sync detection triggers recovery (default: 120 = 2 minutes). -# p2p-stale-sync-timeout-seconds = 120 - -# Pairs of [BLOCK_NUM,BLOCK_ID] that should be enforced as checkpoints. -# checkpoint = - -# Number of threads for rpc-clients. The optimal value is `-1` -webserver-thread-pool-size = 2 - -# IP:PORT for HTTP connections -webserver-http-endpoint = 0.0.0.0:8090 - -# IP:PORT for WebSocket connections -webserver-ws-endpoint = 0.0.0.0:8091 - -# Maximum microseconds for trying to get read lock -read-wait-micro = 500000 - -# Maximum retries to get read lock. Each retry is read-wait-micro microseconds. -# When all retries are made, the rpc-client receives error 'Unable to acquire READ lock'. -max-read-wait-retries = 2 - -# Maximum microseconds for trying to get write lock on broadcast transaction. -write-wait-micro = 500000 - -# Maximum retries to get write lock. Each retry is write-wait-micro microseconds. -# When all retries are made, the rpc-client receives error 'Unable to acquire WRITE lock'. -max-write-wait-retries = 3 - -# Do all write operations (push_block/push_transaction) in the single thread. -# Write lock of database is very heavy. When many threads tries to lock database on writing, rpc-clients -# receive many errors 'Unable to acquire READ lock' ('Unable to acquire WRITE lock'). -# Enabling of this options can increase performance. -single-write-thread = true - -# Enable plugin notifications about operations in a pushed transaction, which should be included to the next generated -# block. Plugins doesn't validate data in operations, they only update its own indexes, so notifications can be -# disabled on push_transaction() without any side-effects. The option doesn't have effect on a pushing signed blocks, -# so it is safe. -# Disabling of this option can increase performance. -enable-plugins-on-push-transaction = false - -# A start size for shared memory file when it doesn't have any data. Possible cases: -# - If shared memory has data and the value is greater then the size of shared_memory.bin, -# the file will be grown to requested size. -# - If shared memory has data and the value is less then the size of shared_memory.bin, nothing happens. -# Changing of this parameter doesn't require the replaying. -shared-file-size = 2G - -# The minimum free space in the shared memory file. When free space reaches the following value, the size of the -# shared_memory.bin increases by the value of inc-shared-file-size. -min-free-shared-file-size = 500M - -# Step of increasing size of shared_memory.bin. When the free memory size reaches min-free-shared-file-size, -# the shared memory size increases by the following value. -inc-shared-file-size = 2G - -# How often do checking the free space in shared_memory.bin. A very frequent checking can decrease performance. -# It's not critical if the free size became very small, because the daemon catches the `bad_alloc` exception -# and resizes. The optimal strategy is do checking of the free space, but not very often. -block-num-check-free-size = 1000 # each 3000 seconds - -plugin = chain p2p json_rpc webserver network_broadcast_api validator test_api database_api account_by_key operation_history account_history block_info raw_block validator_api mongo_db - -# For connect to mongodb which is running outside Docker (if vizd running inside) -mongodb-uri = mongodb://172.17.0.1:27017/viz - -# Remove votes before defined block, should increase performance -clear-votes-before-block = 0 # clear votes after each cashout - -# Virtual operations will not be passed to the plugins, enabling of the option helps to save some memory. -skip-virtual-ops = false - -# Defines a range of accounts to track by the account_history plugin as a json pair ["from","to"] [from,to] -# track-account-range = - -# Defines a list of operations which will be explicitly logged by the account_history plugin. -# history-whitelist-ops = account_create_operation account_update_operation content_operation delete_content_operation vote_operation author_reward_operation curation_reward_operation transfer_operation transfer_to_vesting_operation withdraw_vesting_operation witness_update_operation account_witness_vote_operation account_witness_proxy_operation fill_vesting_withdraw_operation shutdown_witness_operation custom_json_operation request_account_recovery_operation recover_account_operation change_recovery_account_operation escrow_transfer_operation escrow_approve_operation escrow_dispute_operation escrow_release_operation content_benefactor_reward_operation - -# Defines a list of operations which will be explicitly ignored by the account_history plugin. -# history-blacklist-ops = - -# Defines starting block from which recording stats by the account_history plugin. -# history-start-block = 0 - - -# Enable block production, even if the chain is stale. -enable-stale-production = false - - -# Percent of validators (0-99) that must be participating in order to produce blocks -required-participation = 0 - -# name of validator controlled by this node (e.g. initwitness ) -# validator = -# # validator = # DEPRECATED: use 'validator' - -# WIF PRIVATE KEY to be used by one or more validators -# private-key = - -# declare an appender named "stderr" that writes messages to the console -[log.console_appender.stderr] -stream=std_error - -# declare an appender named "p2p" that writes messages to p2p.log -[log.file_appender.p2p] -filename=logs/p2p/p2p.log -# filename can be absolute or relative to this config file - -# route any messages logged to the default logger to the "stderr" logger we -# declared above, if they are info level are higher -[logger.default] -level=debug -appenders=stderr - -# route messages sent to the "p2p" logger to stderr too -[logger.p2p] -level=error -appenders=stderr From fd8c6782f439be3520d2a7e39cf39b8a95709929 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Thu, 21 May 2026 19:22:45 +0400 Subject: [PATCH 04/16] fix(network): replace ilog with dlog for peer connection logging - Changed info-level logs (ilog) to debug-level logs (dlog) when connecting to peers and sending DLT hello messages - Updated rate-limit notification from ilog to dlog for peer exchange requests - Ensured logging reflects appropriate verbosity level for peer communication events --- libraries/network/dlt_p2p_node.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/network/dlt_p2p_node.cpp b/libraries/network/dlt_p2p_node.cpp index 766dc495aa..5ebb49546d 100644 --- a/libraries/network/dlt_p2p_node.cpp +++ b/libraries/network/dlt_p2p_node.cpp @@ -285,7 +285,7 @@ void dlt_p2p_node::connect_to_peer(const fc::ip::endpoint& ep) { // Send hello send_message(pid, message(build_hello_message())); - ilog(DLT_LOG_GREEN "Connected to peer ${ep}, sent DLT hello" DLT_LOG_RESET, ("ep", ep)); + dlog(DLT_LOG_GREEN "Connected to peer ${ep}, sent DLT hello" DLT_LOG_RESET, ("ep", ep)); // Start read loop as a fiber on the p2p thread start_read_loop(pid); @@ -1770,7 +1770,7 @@ void dlt_p2p_node::on_dlt_peer_exchange_reply(peer_id peer, const dlt_peer_excha void dlt_p2p_node::on_dlt_peer_exchange_rate_limited(peer_id peer, const dlt_peer_exchange_rate_limited& msg) { auto it = _peer_states.find(peer); auto ep = (it != _peer_states.end()) ? std::string(it->second.endpoint) : std::to_string(peer); - ilog(DLT_LOG_DGRAY "Peer ${ep} rate-limited our exchange request, wait ${w}s" DLT_LOG_RESET, + dlog(DLT_LOG_DGRAY "Peer ${ep} rate-limited our exchange request, wait ${w}s" DLT_LOG_RESET, ("ep", ep)("w", msg.wait_seconds)); // Record the rate-limit locally so periodic_peer_exchange() stops From 40cd17f3036c2aabb146235efe98e85d68233488 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Thu, 21 May 2026 20:44:30 +0400 Subject: [PATCH 05/16] fix(webserver): add CORS headers and handle preflight OPTIONS requests - Handle CORS preflight by responding to OPTIONS method with proper headers - Append Access-Control-Allow-Origin header to all HTTP responses - Add Access-Control-Allow-Methods, Allow-Headers, and Max-Age headers for OPTIONS responses - Ensure CORS headers are included on error and success responses - Prevent CORS issues for cross-origin API calls through the webserver plugin --- plugins/webserver/webserver_plugin.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugins/webserver/webserver_plugin.cpp b/plugins/webserver/webserver_plugin.cpp index 3c4aadd63b..a37fd1d99d 100644 --- a/plugins/webserver/webserver_plugin.cpp +++ b/plugins/webserver/webserver_plugin.cpp @@ -421,10 +421,22 @@ namespace graphene { auto con = server->get_con_from_hdl(hdl); con->defer_http_response(); + // CORS preflight + if (con->get_request().get_method() == "OPTIONS") { + con->append_header("Access-Control-Allow-Origin", "*"); + con->append_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); + con->append_header("Access-Control-Allow-Headers", "Content-Type, Authorization"); + con->append_header("Access-Control-Max-Age", "86400"); + con->set_status(websocketpp::http::status_code::ok); + try { con->send_http_response(); } catch (...) {} + return; + } + thread_pool_ios.post([con, this]() { auto body = con->get_request_body(); if (body.empty()) { + con->append_header("Access-Control-Allow-Origin", "*"); con->set_body("empty request body"); con->set_status(websocketpp::http::status_code::bad_request); try { con->send_http_response(); } catch (...) {} @@ -439,12 +451,14 @@ namespace graphene { // Invalid JSON — skip cache, let json_rpc handle the error try { api->call(body, [con](const std::string &data){ + con->append_header("Access-Control-Allow-Origin", "*"); con->set_body(data); con->set_status(websocketpp::http::status_code::ok); con->send_http_response(); }); } catch (fc::exception &e) { edump((e)); + con->append_header("Access-Control-Allow-Origin", "*"); con->set_body("Could not call API"); con->set_status(websocketpp::http::status_code::not_found); try { con->send_http_response(); } catch (...) {} @@ -466,6 +480,7 @@ namespace graphene { if (cached_response.valid()) { // Patch the id in cached response to match request std::string patched = patch_response_id(*cached_response, request_id); + con->append_header("Access-Control-Allow-Origin", "*"); con->set_body(patched); con->set_status(websocketpp::http::status_code::ok); con->send_http_response(); @@ -477,6 +492,7 @@ namespace graphene { api->call(body, [con, this, request_hash, cacheable](const std::string &data){ // this lambda can be called from any thread in application // for example, when task was delegated ( see msg_pack(msg_pack&&) ) + con->append_header("Access-Control-Allow-Origin", "*"); con->set_body(data); con->set_status(websocketpp::http::status_code::ok); con->send_http_response(); @@ -489,6 +505,7 @@ namespace graphene { } catch (fc::exception &e) { // this case happens if exception was thrown on parsing request edump((e)); + con->append_header("Access-Control-Allow-Origin", "*"); con->set_body("Could not call API"); con->set_status(websocketpp::http::status_code::not_found); // this sending response can't be merged with sending response from try-block From 5ec792aa9fae12dd375f37c0ccc48872cfe85ab1 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Fri, 22 May 2026 06:59:24 +0400 Subject: [PATCH 06/16] fix(network): prevent log spamming during peer disconnect - Add check to skip logging if disconnect is already in progress for a peer - Avoid re-entrance in send_message calls during handle_disconnect coroutine - Prevent excessive log entries when send queue is at max depth and peer disconnects --- libraries/network/dlt_p2p_node.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/network/dlt_p2p_node.cpp b/libraries/network/dlt_p2p_node.cpp index 5ebb49546d..e032328b8d 100644 --- a/libraries/network/dlt_p2p_node.cpp +++ b/libraries/network/dlt_p2p_node.cpp @@ -514,6 +514,10 @@ void dlt_p2p_node::send_message(peer_id peer, const message& msg) { ++state.send_queue_total; } else { // Queue is at max depth — peer can't consume data fast enough. + // Skip if disconnect is already in progress: handle_disconnect yields + // at cancel_and_wait, allowing other fibers to call send_message for + // the same peer, which would re-enter this branch and spam the log. + if (_disconnect_in_progress.count(peer)) return; // Capture info before handle_disconnect potentially erases the state. std::string ep = std::string(state.endpoint); uint32_t dropped = state.send_queue_dropped; From 35aef463ef6ef5df4587b0eec283b0f5e1ce892a Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Fri, 22 May 2026 07:09:22 +0400 Subject: [PATCH 07/16] fix(network): prevent deadlock by closing socket before cancelling read fiber - Close socket first to unblock pending I/O and avoid multi-second hangs - Erase connection after closing to prevent dangling shared_ptr references - Cancel read fiber only after socket is closed to ensure immediate exit - Retain reentrancy guard to keep peer state valid during disconnect handling - Adjust order of operations to fix deadlock when multiple peers disconnect simultaneously --- libraries/network/dlt_p2p_node.cpp | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/libraries/network/dlt_p2p_node.cpp b/libraries/network/dlt_p2p_node.cpp index e032328b8d..2e11dddac2 100644 --- a/libraries/network/dlt_p2p_node.cpp +++ b/libraries/network/dlt_p2p_node.cpp @@ -342,23 +342,29 @@ void dlt_p2p_node::handle_disconnect(peer_id peer, const std::string& reason, bo } } - // Cancel read fiber — cancel_and_wait yields, allowing drain_send_queue - // to resume on this thread. The reentrancy guard above ensures that - // reentrant handle_disconnect call returns immediately without touching - // _peer_states, so state/it remain valid when we resume here. - auto fiber_it = _read_fibers.find(peer); - if (fiber_it != _read_fibers.end()) { - try { if (fiber_it->second.valid()) fiber_it->second.cancel_and_wait(__FUNCTION__); } catch (...) {} - _read_fibers.erase(fiber_it); - } - - // Close connection + // Close the socket FIRST — this immediately unblocks any pending readsome/writesome + // in the read fiber and drain_send_queue fiber, causing them to throw and exit. + // drain_send_queue holds sock by owning shared_ptr copy, so erasing _connections + // here does not leave it with a dangling reference. + // If we closed AFTER cancel_and_wait, the fiber would be stuck waiting for network + // I/O that can never arrive on a dead peer — causing a multi-second hang per peer, + // and a full deadlock when N peers disconnect simultaneously (p82: silent reboot). auto conn_it = _connections.find(peer); if (conn_it != _connections.end()) { try { if (conn_it->second) conn_it->second->close(); } catch (...) {} _connections.erase(conn_it); } + // Cancel read fiber — cancel_and_wait yields, but the fiber exits immediately + // because its socket I/O is already unblocked by the close() above. + // The reentrancy guard above ensures that reentrant handle_disconnect calls + // return immediately without touching _peer_states, so state/it remain valid. + auto fiber_it = _read_fibers.find(peer); + if (fiber_it != _read_fibers.end()) { + try { if (fiber_it->second.valid()) fiber_it->second.cancel_and_wait(__FUNCTION__); } catch (...) {} + _read_fibers.erase(fiber_it); + } + // Clear send guard and drain any queued messages _peer_sending.erase(peer); state.send_queue.clear(); @@ -542,7 +548,7 @@ void dlt_p2p_node::drain_send_queue(peer_id peer, std::vector buf) { _peer_sending.erase(peer); return; } - auto& sock = conn_it->second; + auto sock = conn_it->second; // owning copy — handle_disconnect may erase _connections while we yield in writesome // Cache endpoint before entering the try block — handle_disconnect may // remove the peer from _peer_states before the catch block runs, making From 4fc71ec1a1e9697b1e37eb955214fd9eaecf3c21 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Fri, 22 May 2026 10:43:14 +0400 Subject: [PATCH 08/16] docs(introduction): add community symbol and display conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduced Ƶ as the short symbol for VIZ chosen by the community - Explained common practice of showing balances with 2 decimal places - Noted that even staked funds (SHARES) are displayed as Ƶ with staking notes - Clarified symbol usage in wallets, explorers, and applications docs(webserver): document native CORS support in webserver plugin - Detailed handling of browser cross-origin requests without reverse proxy - Specified preflight (OPTIONS) response headers and values - Confirmed all other responses include Access-Control-Allow-Origin: * - Mentioned compatibility with production setups using nginx proxy - Highlighted use cases for browser-based wallets and dApps calling JSON-RPC endpoints directly --- docs/introduction/key-concepts.md | 6 ++++++ docs/plugins/webserver.md | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/docs/introduction/key-concepts.md b/docs/introduction/key-concepts.md index 3dd3154150..e6b53d3e26 100644 --- a/docs/introduction/key-concepts.md +++ b/docs/introduction/key-concepts.md @@ -45,6 +45,12 @@ An authority is a multi-sig structure: `{ weight_threshold, account_auths[], key - Created by staking VIZ; withdrawn back to VIZ over 28 intervals (≈28 days) - Not directly transferable; can be delegated to other accounts +### Community Symbol: Ƶ + +The community has chosen **Ƶ** as the short symbol for VIZ. Most wallets, explorers, and applications display it instead of the full ticker. + +It is also common practice to show balances with **2 decimal places** regardless of the underlying token type. Even staked funds (SHARES) are often displayed as `Ƶ` with a note that they are staked in the account, rather than switching to the `SHARES` unit and its 6-decimal format. + --- ## Energy System diff --git a/docs/plugins/webserver.md b/docs/plugins/webserver.md index c8262af9f9..64f56709af 100644 --- a/docs/plugins/webserver.md +++ b/docs/plugins/webserver.md @@ -93,6 +93,25 @@ Subscriptions require a persistent WebSocket connection. They are not available --- +## CORS + +The webserver plugin handles browser cross-origin requests natively — no reverse proxy is required for local or development setups. + +**Preflight requests** (`OPTIONS`) are answered immediately with: + +| Response header | Value | +|----------------|-------| +| `Access-Control-Allow-Origin` | `*` | +| `Access-Control-Allow-Methods` | `POST, GET, OPTIONS` | +| `Access-Control-Allow-Headers` | `Content-Type, Authorization` | +| `Access-Control-Max-Age` | `86400` | + +**All other HTTP responses** include `Access-Control-Allow-Origin: *`. + +This allows browser-based wallets and dApps to call the JSON-RPC endpoint directly. For production deployments behind nginx, CORS is handled at the proxy layer (see [Exposing the API via HTTPS](#exposing-the-api-via-https-nginx--certbot)) — both layers setting the header is harmless. + +--- + ## Security - **Bind to localhost** (`127.0.0.1`) and use a reverse proxy (nginx/Caddy) for public exposure. Binding to `0.0.0.0` exposes the RPC directly to the network. From 3e344695f3cc091c5a7ca37f2b793548f59125c3 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Sat, 23 May 2026 20:41:50 +0400 Subject: [PATCH 09/16] chain: clear currently_syncing after auto-recovery completes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After shared-memory corruption triggers attempt_auto_recovery(), the function sets currently_syncing=true so the validator plugin defers block production during the wipe / snapshot import / dlt_block_log replay sequence. Once the database is rebuilt and P2P is resumed, the flag was expected to self-clear on the next applied block via plugin_impl::accept_block(), which stores the caller-supplied sync_mode flag whenever a block is successfully pushed. That self-clearing path never runs on the DLT pipeline. The DLT P2P delegate (dlt_delegate::accept_block in plugins/p2p/p2p_plugin.cpp) calls chain.db().push_block() directly and bypasses plugin_impl::accept_block() entirely, so neither broadcast blocks nor gap-fill replies ever update currently_syncing. The only remaining clearer is transition_to_forward(), but a node that was in FORWARD mode at the moment of corruption stays in FORWARD throughout pause/resume — transition_to_forward() is never invoked, so the flag is permanently stuck at true. The validator gate at plugins/validator/validator.cpp checks chain().is_syncing() in DLT mode and returns not_synced, producing the observed indefinite "Block production deferred: not_synced (head=#X, catching_up=false)" loop where head keeps advancing via P2P but no local block is produced. Fix: explicitly clear currently_syncing immediately after do_snapshot_load(data_dir, true) returns successfully in attempt_auto_recovery(). Post-recovery catchup remains correctly gated by _catchup_after_pause in the P2P layer, which the periodic task clears once no peer is ahead of our head. --- plugins/chain/plugin.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plugins/chain/plugin.cpp b/plugins/chain/plugin.cpp index 4ed946d25d..84d748692e 100644 --- a/plugins/chain/plugin.cpp +++ b/plugins/chain/plugin.cpp @@ -946,6 +946,19 @@ namespace chain { wlog("=== AUTO-RECOVERY COMPLETE: node resumed at block ${n} ===", ("n", my->db.head_block_num())); + // Recovery is complete: clear the syncing flag so the validator + // plugin can resume block production once the post-pause catchup + // window closes. The DLT P2P delegate calls db.push_block() + // directly and bypasses plugin_impl::accept_block(), so the + // flag-update path that would otherwise self-clear this on the + // next applied block never runs on the DLT path. Without this + // explicit reset, the flag set above stays true forever and + // is_syncing() permanently gates production with not_synced. + // The remaining catchup window is gated by _catchup_after_pause + // in the P2P layer, which clears itself once peers are no longer + // ahead of our head. + my->currently_syncing.store(false, std::memory_order_relaxed); + // 5. Resume P2P now that the database is fully rebuilt. // do_snapshot_load(is_recovery=true) already set LIB = head // so P2P will request blocks after the snapshot head. From f5d0d46d9bac97f9021c79caca7981f4923ef8a0 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Sat, 23 May 2026 21:53:57 +0400 Subject: [PATCH 10/16] fix(snapshot): defer wake-up until block strictly after validator slot The deferred-snapshot wake-up in on_applied_block previously used head_block_time() >= pending_snapshot_safe_after_time, which fires on the very block the local validator just produced. The applied_block signal is dispatched synchronously from _push_block inside db.generate_block(), and the validator only calls p2p().broadcast_block() after generate_block() returns. So firing the snapshot on the same block let the snapshot read-lock start before the produced block had been broadcast to peers. Change the condition to strictly greater than: the deferred snapshot now waits until a SUBSEQUENT block is applied. That block is built by another validator on top of ours, proving our block was produced, applied locally, and propagated through the network. Only then does the snapshot start reading state. Cost is ~one block interval of additional delay, and only on slots where the local validator was the deferral target. The non-producer path is unchanged: snapshots still fire immediately at the originating block when is_validator_producing_soon() is false. Also expanded the surrounding comment block and updated the wake-up log messages to reflect the new semantics. --- plugins/snapshot/plugin.cpp | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/plugins/snapshot/plugin.cpp b/plugins/snapshot/plugin.cpp index d4e9177caf..4082b54433 100644 --- a/plugins/snapshot/plugin.cpp +++ b/plugins/snapshot/plugin.cpp @@ -1973,26 +1973,38 @@ void snapshot_plugin::plugin_impl::on_applied_block(const graphene::protocol::si // deferred. We do NOT re-check is_validator_producing_soon() here to avoid an // infinite deferral loop where the validator is always scheduled soon. // - // Instead, we wait for the specific validator slot to be filled: the deferred - // snapshot only fires once head_block_time() >= pending_snapshot_safe_after_time, - // meaning the validator's block has been produced and applied (or the slot was - // missed and the chain moved past it). This prevents the snapshot from starting - // while the validator is about to produce. + // We wait for a block to be applied that is STRICTLY AFTER the validator slot + // we deferred for: head_block_time() > pending_snapshot_safe_after_time. + // + // Why strictly greater (not >=): + // The applied_block signal is dispatched synchronously inside _push_block, + // BEFORE generate_block() returns to the validator and BEFORE the validator + // calls p2p().broadcast_block(). If we fired the snapshot on the same block + // the local validator just produced, the snapshot read-lock could start + // before the produced block has been broadcast to peers. + // + // Requiring head_block_time > slot_time means we wait until a SUBSEQUENT + // block is applied. That block is necessarily produced by another validator + // on top of ours, which proves our block was successfully produced, applied + // locally, and propagated through the network. Only then is it safe to + // start the snapshot read pass. + // + // Cost: ~one block interval of additional delay, but only when the local + // validator was the deferral target. When we are not the producer, the + // snapshot fires immediately at the originating block (no deferral path). if (snapshot_pending && !is_syncing) { // If safe_after_time is epoch (lookup failed), fire immediately as fallback. - // If head_block_time has reached/passed the validator slot time, the block - // at that slot has been applied (or the slot was skipped by a gap). bool safe_to_fire = (pending_snapshot_safe_after_time == fc::time_point_sec()) || - (db.head_block_time() >= pending_snapshot_safe_after_time); + (db.head_block_time() > pending_snapshot_safe_after_time); if (safe_to_fire) { fc::path output(pending_snapshot_path); snapshot_pending = false; pending_snapshot_path.clear(); pending_snapshot_safe_after_time = fc::time_point_sec(); - ilog(CLOG_GREEN "Creating deferred snapshot now (validator slot passed): ${p}" CLOG_RESET, ("p", output.string())); + ilog(CLOG_GREEN "Creating deferred snapshot now (validator slot passed and block broadcast): ${p}" CLOG_RESET, ("p", output.string())); schedule_async_snapshot(output, "deferred"); } else { - dlog("Deferred snapshot waiting for validator slot at ${t} (head_block_time=${h})", + dlog("Deferred snapshot waiting for block strictly after validator slot ${t} (head_block_time=${h})", ("t", pending_snapshot_safe_after_time)("h", db.head_block_time())); } } From f31326667b6a7700266126e6aed7218f6e90b0d2 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Mon, 25 May 2026 17:23:53 +0400 Subject: [PATCH 11/16] fix: show correct next scheduled validator in missed block log Replace hardcoded b.validator with get_scheduled_validator(i + 2) so each missed block line shows the validator scheduled for the slot immediately after the miss, instead of repeating the current block producer for every line. --- libraries/chain/database.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/chain/database.cpp b/libraries/chain/database.cpp index 10fafcb33b..4f75234642 100644 --- a/libraries/chain/database.cpp +++ b/libraries/chain/database.cpp @@ -5500,7 +5500,7 @@ namespace graphene { namespace chain { ("w", validator_missed.owner) ("n", head_block_num() + i + 1) ("t", get_slot_time(i + 1)) - ("next", b.validator)); + ("next", get_scheduled_validator(i + 2))); } modify(validator_missed, [&](validator_object &w) { From bf42081070eee882f8b0adbeaaa13d1812658295 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Mon, 25 May 2026 17:43:05 +0400 Subject: [PATCH 12/16] fix(auto-recovery): reset recovery_in_progress flag and prevent SYNC/FORWARD oscillation The static atomic recovery_in_progress flag in attempt_auto_recovery() was never reset to false after successful recovery, making any subsequent corruption event permanently unrecoverable ("already in progress, skipping duplicate attempt"). Reset it after P2P resume so the node can recover from future corruption events. Add a consecutive recovery counter (max 3 within 5 minutes) to prevent infinite recovery loops when the snapshot or block log is itself corrupted. In request_gap_fill(), remove the SYNC transition and peer request loop from the "no peer available" fallback path. When no peer has a higher head, transition_to_sync() followed by request_blocks_from_peer() immediately detects all peers as "caught up" and calls transition_to_forward(), producing rapid SYNC->FORWARD oscillation every 5 seconds. Instead, just log and let the periodic task retry when new peers connect. --- libraries/network/dlt_p2p_node.cpp | 21 +++++++----------- plugins/chain/plugin.cpp | 35 +++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/libraries/network/dlt_p2p_node.cpp b/libraries/network/dlt_p2p_node.cpp index 2e11dddac2..55462bcebc 100644 --- a/libraries/network/dlt_p2p_node.cpp +++ b/libraries/network/dlt_p2p_node.cpp @@ -2131,21 +2131,16 @@ void dlt_p2p_node::request_gap_fill() { ("ep", peer_state.endpoint)("ph", peer_state.peer_head_num)("ex", peer_state.exchange_enabled)); send_message(any_active_peer, message(req)); } else { - // P39 fix: No peer at all with a higher head — gap fill - // can't help. Transition to SYNC immediately instead of - // waiting for stagnation detection. - wlog("Gap fill: no peer available — transitioning to SYNC"); + // No peer with a higher head is available for gap fill. + // Do NOT transition to SYNC — without a peer ahead of us, + // request_blocks_from_peer() would immediately see all peers + // as "caught up" and call transition_to_forward(), producing + // rapid SYNC→FORWARD oscillation. Instead, just log and let + // the periodic task retry when new peers connect or existing + // peers advance their head. + wlog("Gap fill: no peer available with higher head — waiting for peers"); _gap_fill_in_progress = false; _gap_fill_start_time = fc::time_point(); - transition_to_sync(); - // Request blocks from all active peers - for (const auto& _pi : _peer_states) { - const auto& state = _pi.second; - if (state.lifecycle_state == DLT_PEER_LIFECYCLE_ACTIVE || - state.lifecycle_state == DLT_PEER_LIFECYCLE_SYNCING) { - request_blocks_from_peer(_pi.first); - } - } } } diff --git a/plugins/chain/plugin.cpp b/plugins/chain/plugin.cpp index 84d748692e..06ba79df5a 100644 --- a/plugins/chain/plugin.cpp +++ b/plugins/chain/plugin.cpp @@ -884,13 +884,40 @@ namespace chain { void plugin::attempt_auto_recovery() { static std::atomic recovery_in_progress{false}; + static constexpr int MAX_CONSECUTIVE_RECOVERIES = 3; + static constexpr int RECOVERY_COOLDOWN_SEC = 300; // 5 minutes + static int consecutive_recoveries = 0; + static fc::time_point last_recovery_time; + bool expected = false; if (!recovery_in_progress.compare_exchange_strong(expected, true)) { wlog("Auto-recovery already in progress, skipping duplicate attempt"); return; } - wlog("=== IMMEDIATE AUTO-RECOVERY: shared memory corruption detected ==="); + // Guard against infinite recovery loops: if the same block keeps + // failing after recovery, the snapshot or block log may be corrupted. + // Reset the counter after a cooldown period to allow eventual retry. + auto now = fc::time_point::now(); + if (last_recovery_time != fc::time_point() && + (now - last_recovery_time).to_seconds() > RECOVERY_COOLDOWN_SEC) { + consecutive_recoveries = 0; + } + consecutive_recoveries++; + last_recovery_time = now; + + if (consecutive_recoveries > MAX_CONSECUTIVE_RECOVERIES) { + elog("Auto-recovery limit reached: ${n} consecutive attempts within ${c}s cooldown. " + "The snapshot or block log may be corrupted — manual intervention required. " + "Try a fresh snapshot or delete the block log.", + ("n", consecutive_recoveries)("c", RECOVERY_COOLDOWN_SEC)); + recovery_in_progress.store(false, std::memory_order_release); + appbase::app().quit(); + return; + } + + wlog("=== IMMEDIATE AUTO-RECOVERY: shared memory corruption detected (attempt ${n}/${max}) ===", + ("n", consecutive_recoveries)("max", MAX_CONSECUTIVE_RECOVERIES)); // 1. Find latest snapshot fc::path snap = my->find_latest_snapshot(); @@ -971,6 +998,12 @@ namespace chain { } catch (...) { wlog("Auto-recovery: failed to resume P2P"); } + + // Allow future recovery attempts. Without this reset the + // static atomic stays true forever and any subsequent + // corruption event is silently discarded, leaving the node + // permanently stuck. + recovery_in_progress.store(false, std::memory_order_release); } catch (const fc::exception& e) { elog("Auto-recovery FAILED during snapshot load: ${e}", ("e", e.to_detail_string())); appbase::app().quit(); From a5d8e61ee46616bed280f0f1951f93d7feb9e5d6 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Mon, 25 May 2026 17:49:28 +0400 Subject: [PATCH 13/16] Add diagnostic logs to database::open() startup path Node crashes silently between DLT block log open and "Done opening block log" with no error output. Add step-by-step ilog() calls to every major operation in the critical path so the exact failing step is visible in the next crash log: - block_log and dlt_block_log head after open - Before/after undo_all() with revision values - Revision mismatch detection with values - Before reading head block from block_log - fork_db seeding start in both normal and DLT modes - Before/after init_hardforks() (second call) - Before validator schedule integrity check Also add db.open() success log in chain plugin_startup. --- libraries/chain/database.cpp | 20 ++++++++++++++++---- plugins/chain/plugin.cpp | 2 ++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/libraries/chain/database.cpp b/libraries/chain/database.cpp index 4f75234642..3f39241148 100644 --- a/libraries/chain/database.cpp +++ b/libraries/chain/database.cpp @@ -253,9 +253,12 @@ namespace graphene { namespace chain { } _block_log.open(data_dir / "block_log"); + ilog("block_log opened, head=${h}", ("h", _block_log.head() ? std::to_string(_block_log.head()->block_num()) : std::string("none"))); _dlt_block_log.open(data_dir / "dlt_block_log"); + ilog("dlt_block_log opened, head=${h}", ("h", _dlt_block_log.head() ? std::to_string(_dlt_block_log.head_block_num()) : std::string("none"))); // Rewind all undo state. This should return us to the state at the last irreversible block. + ilog("Calling undo_all()..."); // Wrap in a try-catch for boost::interprocess::lock_exception: // After a hard crash, the previous process may have died while holding // shared-memory internal mutexes (e.g., inside managed_mapped_file allocator). @@ -275,8 +278,12 @@ namespace graphene { namespace chain { "Shared memory lock corrupted (previous crash): ${what}", ("what", e.what())); } + ilog("undo_all() completed, revision=${rev} head_block_num=${hbn}", + ("rev", revision())("hbn", head_block_num())); if (revision() != head_block_num()) { + ilog("Revision mismatch: revision=${rev} != head_block_num=${hbn}, calling init_hardforks()", + ("rev", revision())("hbn", head_block_num())); with_strong_read_lock([&]() { init_hardforks(); // Writes to local state, but reads from db }); @@ -290,6 +297,7 @@ namespace graphene { namespace chain { } if (head_block_num()) { + ilog("Validating block log consistency, head_block_num=${h}", ("h", head_block_num())); // Validate DLT block log consistency before seeding fork_db. // After a crash, the DLT block log index/data files can become // truncated (e.g., only 1 block when database has thousands). @@ -303,14 +311,16 @@ namespace graphene { namespace chain { _dlt_block_log.reset(); } + ilog("Reading head block #${n} from block_log", ("n", head_block_num())); auto head_block = _block_log.read_block_by_num(head_block_num()); if (head_block.valid()) { // Block_log has the head block FC_ASSERT(head_block->id() == head_block_id(), "Chain state does not match block log. Please reindex blockchain."); + ilog("Head block found in block_log, starting fork_db and seeding"); _fork_db.start_block(*head_block); - // P22 fix: Seed fork_db with recent blocks (up to 100) + // Seed fork_db with recent blocks (up to 100) // so that incoming sync blocks from peers near our head // can find their parent chain. After restart, fork_db only // has the head block; if peers send blocks a few behind @@ -343,9 +353,8 @@ namespace graphene { namespace chain { } else { // DLT mode: block_log is empty but chainbase has state (loaded from snapshot). set_dlt_mode(true); - wlog("DLT mode detected: block log is empty but database has state at block ${n}. " - "Skipping block log validation.", - ("n", head_block_num())); + ilog("DLT mode: block_log empty, seeding fork_db from DLT log for head_block_num=${h}", + ("h", head_block_num())); // Seed fork_db bottom-up from the oldest available DLT block // within a seeding window so that all blocks from oldest to @@ -424,11 +433,14 @@ namespace graphene { namespace chain { wlog("Done opening block log, elapsed time ${t} sec", ("t", double((end - start).count()) / 1000000.0)); } + ilog("Block log open complete, calling init_hardforks()"); with_strong_read_lock([&]() { init_hardforks(); // Writes to local state, but reads from db }); + ilog("init_hardforks() completed"); // === HARDFORK 12: EMERGENCY SCHEDULE RECOVERY === + ilog("Checking validator schedule integrity at head_block_num=${h}", ("h", head_block_num())); // If the node shut down (or crashed) during emergency mode while // update_validator_schedule() had zeroed the schedule but before the // hybrid override could fill it with committee, the schedule may diff --git a/plugins/chain/plugin.cpp b/plugins/chain/plugin.cpp index 06ba79df5a..c90ec87233 100644 --- a/plugins/chain/plugin.cpp +++ b/plugins/chain/plugin.cpp @@ -631,7 +631,9 @@ namespace chain { try { ilog("Opening shared memory from ${path}", ("path", my->shared_memory_dir.generic_string())); my->db.open(data_dir, my->shared_memory_dir, CHAIN_INIT_SUPPLY, my->shared_memory_size, chainbase::database::read_write/*, my->validate_invariants*/ ); + ilog("db.open() completed successfully, head_block_num=${h}", ("h", my->db.head_block_num())); auto head_block_log = my->db.get_block_log().head(); + ilog("block_log head=${h}", ("h", head_block_log ? std::to_string(head_block_log->block_num()) : std::string("none"))); my->replay |= head_block_log && my->db.revision() != head_block_log->block_num(); if (my->replay) { From 1a2d8880f692bd647d9470200363ecbd0d4912c1 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Mon, 25 May 2026 19:17:50 +0400 Subject: [PATCH 14/16] fix(memory-order): upgrade currently_syncing atomics to release/acquire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All reads and writes to the currently_syncing atomic flag used relaxed ordering, which does not guarantee cross-thread visibility on non-x86 architectures. The recovery thread writes currently_syncing=false after rebuilding the database, and the validator production thread reads it to decide whether to produce blocks. Upgrade to release/acquire ordering to ensure the store is visible to the reader on all platforms. store → memory_order_release (3 sites) load → memory_order_acquire (1 site) exchange → memory_order_acq_rel (1 site) --- plugins/chain/plugin.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/chain/plugin.cpp b/plugins/chain/plugin.cpp index c90ec87233..1cdfebb000 100644 --- a/plugins/chain/plugin.cpp +++ b/plugins/chain/plugin.cpp @@ -139,7 +139,7 @@ namespace chain { // particularly when the emergency master receives blocks from a // competing fork that have gap=0 but previous != head_block_id. if (block_applied) { - currently_syncing.store(currently_syncing_flag, std::memory_order_relaxed); + currently_syncing.store(currently_syncing_flag, std::memory_order_release); if (currently_syncing_flag) { if (!sync_start_logged) { ilog("\033[92m>>> Syncing Blockchain started from block #${n} (head: ${head})\033[0m", @@ -280,11 +280,11 @@ namespace chain { } bool plugin::is_syncing() const { - return my->currently_syncing.load(std::memory_order_relaxed); + return my->currently_syncing.load(std::memory_order_acquire); } void plugin::clear_syncing() { - if (my->currently_syncing.exchange(false, std::memory_order_relaxed)) { + if (my->currently_syncing.exchange(false, std::memory_order_acq_rel)) { ilog("Sync complete: cleared currently_syncing flag (validator block production may resume)"); my->sync_start_logged = false; } @@ -955,7 +955,7 @@ namespace chain { } // Mark syncing so witness plugin defers block production during recovery. - my->currently_syncing.store(true, std::memory_order_relaxed); + my->currently_syncing.store(true, std::memory_order_release); wlog("Auto-recovery: closing database and recovering from snapshot ${p}...", ("p", snap.string())); @@ -986,7 +986,7 @@ namespace chain { // The remaining catchup window is gated by _catchup_after_pause // in the P2P layer, which clears itself once peers are no longer // ahead of our head. - my->currently_syncing.store(false, std::memory_order_relaxed); + my->currently_syncing.store(false, std::memory_order_release); // 5. Resume P2P now that the database is fully rebuilt. // do_snapshot_load(is_recovery=true) already set LIB = head From 6c6d7a45d9f45177970240d14b828a20e9581e04 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Mon, 25 May 2026 19:24:43 +0400 Subject: [PATCH 15/16] Add crash detection for undo_all() using marker file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit undo_all() in database::open() causes a silent SIGSEGV when shared memory is corrupted after a hard crash. Since segfaults bypass all C++ exception handlers, the node enters an infinite restart loop in Docker without ever reaching the recovery path. Introduce a marker file (state/undo_all_in_progress) that is created before undo_all() and removed after it completes. If the process crashes inside undo_all(), the marker survives and triggers database_revision_exception on the next startup, which activates the existing snapshot recovery path. Marker cleanup is added to: - database::open() — removed after successful undo_all() - database::open_from_snapshot() — cleaned before snapshot import - database::wipe() — cleaned during shared memory wipe --- libraries/chain/database.cpp | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/libraries/chain/database.cpp b/libraries/chain/database.cpp index 3f39241148..cd8652d3ed 100644 --- a/libraries/chain/database.cpp +++ b/libraries/chain/database.cpp @@ -258,6 +258,25 @@ namespace graphene { namespace chain { ilog("dlt_block_log opened, head=${h}", ("h", _dlt_block_log.head() ? std::to_string(_dlt_block_log.head_block_num()) : std::string("none"))); // Rewind all undo state. This should return us to the state at the last irreversible block. + // + // Crash guard: undo_all() walks shared-memory data structures that may + // be corrupted after a hard crash (SIGSEGV). Since a segfault kills the + // process instantly, no C++ exception handler can catch it. We use a + // marker file to detect that a previous run died inside undo_all() and + // throw database_revision_exception to trigger the recovery path instead. + auto undo_marker = shared_mem_dir / "undo_all_in_progress"; + if (boost::filesystem::exists(undo_marker)) { + wlog("Detected incomplete undo_all from previous startup. " + "Shared memory is likely corrupted. " + "Throwing revision mismatch to trigger recovery."); + FC_THROW_EXCEPTION(database_revision_exception, + "Shared memory corrupted: previous undo_all() crashed (marker detected)"); + } + + // Write marker — will be removed after undo_all() succeeds. + // If the process crashes inside undo_all(), the marker survives + // and triggers recovery on the next startup. + { std::ofstream f(undo_marker.string()); } ilog("Calling undo_all()..."); // Wrap in a try-catch for boost::interprocess::lock_exception: // After a hard crash, the previous process may have died while holding @@ -278,6 +297,8 @@ namespace graphene { namespace chain { "Shared memory lock corrupted (previous crash): ${what}", ("what", e.what())); } + // undo_all() completed successfully — remove the crash marker. + boost::filesystem::remove(undo_marker); ilog("undo_all() completed, revision=${rev} head_block_num=${hbn}", ("rev", revision())("hbn", head_block_num())); @@ -513,6 +534,15 @@ namespace graphene { namespace chain { _dlt_mode = true; // Set before init_genesis so all subsequent code sees DLT mode + // Clean up undo_all crash marker if present from a previous failed startup. + // chainbase::database::wipe() only removes shared_memory.bin, not other files + // in the directory, so we must do this explicitly. + auto undo_marker = shared_mem_dir / "undo_all_in_progress"; + if (boost::filesystem::exists(undo_marker)) { + wlog("Removing stale undo_all crash marker before snapshot import"); + boost::filesystem::remove(undo_marker); + } + // Always wipe shared memory before snapshot import to ensure clean state. // This prevents conflicts if: // - A previous snapshot import attempt failed mid-way @@ -909,6 +939,11 @@ namespace graphene { namespace chain { void database::wipe(const fc::path &data_dir, const fc::path &shared_mem_dir, bool include_blocks) { close(); chainbase::database::wipe(shared_mem_dir); + // Remove undo_all crash marker if present (chainbase::wipe only removes shared_memory.bin) + auto undo_marker = shared_mem_dir / "undo_all_in_progress"; + if (boost::filesystem::exists(undo_marker)) { + boost::filesystem::remove(undo_marker); + } if (include_blocks) { fc::remove_all(data_dir / "block_log"); fc::remove_all(data_dir / "block_log.index"); From 3af3ebad8e9489cc54723de4ea8593f80457bf81 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Mon, 25 May 2026 21:29:33 +0400 Subject: [PATCH 16/16] docs: remove CORS from nginx examples --- @l10n/ru/docs/plugins/webserver.md | 16 ---------------- @l10n/zh-CN/docs/plugins/webserver.md | 16 ---------------- docs/plugins/webserver.md | 16 ---------------- 3 files changed, 48 deletions(-) diff --git a/@l10n/ru/docs/plugins/webserver.md b/@l10n/ru/docs/plugins/webserver.md index 3284e98243..4ec83ecd93 100644 --- a/@l10n/ru/docs/plugins/webserver.md +++ b/@l10n/ru/docs/plugins/webserver.md @@ -137,22 +137,6 @@ server { } location / { - # CORS — разрешить любой источник (публичный API) - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; - add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; - - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; - add_header 'Access-Control-Max-Age' 1728000; - add_header 'Content-Type' 'text/plain charset=UTF-8'; - add_header 'Content-Length' 0; - return 204; - } - proxy_pass http://127.0.0.1:8090; proxy_http_version 1.1; diff --git a/@l10n/zh-CN/docs/plugins/webserver.md b/@l10n/zh-CN/docs/plugins/webserver.md index 584d58faa1..e21c91c5d1 100644 --- a/@l10n/zh-CN/docs/plugins/webserver.md +++ b/@l10n/zh-CN/docs/plugins/webserver.md @@ -138,22 +138,6 @@ server { } location / { - # CORS — 允许任意来源(公开 API) - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; - add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; - - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; - add_header 'Access-Control-Max-Age' 1728000; - add_header 'Content-Type' 'text/plain charset=UTF-8'; - add_header 'Content-Length' 0; - return 204; - } - proxy_pass http://127.0.0.1:8090; proxy_http_version 1.1; diff --git a/docs/plugins/webserver.md b/docs/plugins/webserver.md index 64f56709af..f18e5a4b9a 100644 --- a/docs/plugins/webserver.md +++ b/docs/plugins/webserver.md @@ -156,22 +156,6 @@ server { } location / { - # CORS — allow any origin (public API) - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; - add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; - - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; - add_header 'Access-Control-Max-Age' 1728000; - add_header 'Content-Type' 'text/plain charset=UTF-8'; - add_header 'Content-Length' 0; - return 204; - } - proxy_pass http://127.0.0.1:8090; proxy_http_version 1.1;