Все дефекты, которые в прошлой статье я отложил до версии 1.2, исправлены. В ходе работы нашлись и были исправлены ещё два дефекта: баг в самой ноде и нестабильный тест. Так же, у graphene-core теперь есть рабочий Docker-образ: его можно скачать одной командой.
Что было обещано
В прошлой статье «Большое обновление graphene-core» я рассказал о переходе с версии 1.0 на 1.1: новый тулчейн, Ubuntu 26.04, исправленные падения ноды. В конце был раздел «Найдено уже после выпуска и отложено до следующей версии». В нём описывались четыре дефекта:
- база пиров стирается при каждом штатном завершении;
--versionпоказывает древний тег BitShares;cli_walletне подключается поwss://;- исходящие websocket-кадры уходят с нулевой маской.
В планах там же стояла Docker-сборка. Ниже будет написано, что из этого сделано.
Исправленные баги
Все правки лежат в основной ветке graphene форков carbon-witness/graphene-core и carbon-witness/graphene-fc. Каждый баг сначала воспроизводился на старом бинарнике, затем проверялся на новом.
Зависимости ноды теперь тоже полностью собираются из наших форков carbon-witness. К graphene-fc, websocketpp и editline в 1.2.0 добавился форк secp256k1-zkp – криптографической библиотеки, которую раньше брали напрямую из репозитория BitShares. Код библиотеки не менялся.
1. База пиров больше не затирается
При каждой остановке P2P-часть ноды закрывалась три раза: при штатной остановке приложения, в его деструкторе и в деструкторе самого P2P-узла. Первое закрытие сохраняло список пиров в p2p/peers.json и очищало его в памяти. Второе и третье записывали поверх уже пустой список []. В итоге, нода каждый раз стартовала с зашитых в бинарник сидов.
- Исправление: файл записывается только при первом закрытии после открытия базы.
- Проверка: со старым бинарником после остановки в файле
[]. С новым - живые пиры. Добавлен тест, который на старом коде падает. - В Docker-контейнере после
docker stopвpeers.jsonостаётся более 10 пиров.
2. Правильная версия в --version
Версия бралась из git describe --tags, а в истории остались теги BitShares. Отсюда строки вроде 2.0.171025-minor-fix-1-… или просто unknown.
- Номер выпуска теперь задан в коде:
1.2.0. - Строка сборки - версия плюс 8 символов хеша коммита, например
1.2.0-78b4229a. Из архива без git - просто1.2.0. witness_node --versionиcli_wallet --versionпечатают версию, хеш, время сборки и версии OpenSSL, Boost и websocketpp.
Пример:
Version: 1.2.0
Build: 1.2.0-8a4c1123
SHA: 8a4c1123c50183e6d604598920787727ce61bd9c
Timestamp: 70 hours ago
SSL: OpenSSL 3.5.5 27 Jan 2026
Boost: 1.90 Websocket++: 0.7.0
- Попутно: при упакованных git-ссылках вместо хеша коммита подставлялось имя ветки, это тоже исправлено. А в P2P нода теперь представляется как Graphene, а не как BitShares.
3. cli_wallet подключается по wss://
TLS-контекст в библиотеке fc был жёстко задан как TLS 1.0. OpenSSL 3.5 такую версию запрещает, поэтому клиент не отправлял даже приветствие. TLS RPC самой ноды тоже не принимал соединений.
- Исправление: теперь поддерживаются TLS 1.2 и 1.3: при подключении выбирается самая новая версия, которую знают обе стороны. Устаревшие TLS 1.0 и 1.1 отключены.
- Поправка к прошлой статье: SNI на самом деле не был сломан. Проблема была в версии TLS.
- Проверка:
cli_wallet→witness_nodeпоwssработает на TLS 1.3. Сертификат на чужое имя, неизвестный CA и сервер только с TLS 1.0 отвергаются, как и должны.
4. Websocket-кадры клиента снова маскируются
Клиенты в fc были собраны на серверных настройках websocketpp, где генератор случайных чисел всегда возвращает ноль. Поэтому каждый кадр уходил с ключом 00 00 00 00. RFC 6455 требует случайный ключ, и строгие прокси такие кадры отбрасывают.
- Исправление: у клиентов свои конфиги с настоящим генератором случайных чисел.
- Проверка: зонд печатает ключ маски каждого кадра. У старого
cli_wallet–00000000, у нового – случайный, и поws, и поwss.
Найдено дополнительно
Эти два дефекта нашлись уже в ходе работы над 1.2, в списке прошлой статьи их не было.
1) Нестабильный тест сети из двух нод. Тест two_node_network падал примерно в половине прогонов. Причин оказалось четыре:
- у двух нод мог получиться разный genesis и, следовательно, разный chain ID;
- порт второй ноды после прогона минуту висел в
TIME-WAIT; - транзакция терялась, если её рассылали, пока сосед ещё синхронизируется;
- баг в самой ноде, о нём ниже.
После правок: 100 из 100 одиночных прогонов и 20 из 20 под полной нагрузкой процессора.
2) Нода с --seed-node первые 30 секунд не могла подключиться к сидам. Параметр --seed-node задаёт ноды, к которым нужно подключиться при старте. Нода стучалась к ним слишком рано, до запуска своей P2P-части. Сид успевал ответить, но нода ещё не была готова принимать соединения и отбрасывала ответ. Следующая попытка была только через 30 секунд. Баг нашли по логам упавших тестов, но он касается и обычных нод.
- Исправление: соединения с
--seed-nodeоткрываются после запуска P2P. - Результат: нода с явно указанными сидами больше не теряет первые полминуты впустую.
Обновлён список сидов
Сиды – это ноды, адреса которых зашиты в программу: к ним новая нода подключается при первом запуске, чтобы найти остальную сеть. Из 14 сидов в списке отвечали только три: остальные 11 ip-адресов с 24 сентября не приняли ни одного подключения от нашей ноды. Мёртвые адреса удалены, вместо них добавлены три живые ноды сети, которые принимают подключения на постоянном порту.
Docker
Добавлена поддержка Docker. Ноду Graphene теперь можно запустить из готового образа без сборки. Образ публикуется в два реестра и скачивается без логина. Сейчас актуальна версия 1.2.0. Есть три способа установки, от самого быстрого к самому гибкому.
Вариант 1. Образ с Docker Hub
docker pull carbonwitness/graphene-core:1.2.0
docker run -d --name graphene --stop-timeout 300 \
-v graphene-data:/var/lib/graphene -p 1776:1776 -p 127.0.0.1:8090:8090 \
carbonwitness/graphene-core:1.2.0
Блокчейн хранится в томе graphene-data. latest – это последний релиз. Каждый релиз также помечен своей версией, например 1.2.0, а релиз-кандидаты – только своей, например 1.2.0-rc2.
Вариант 2. Образ из GitHub Container Registry
Это тот же образ, что и на Docker Hub, его публикует та же сборка. Пригодится, если Docker Hub ограничивает число скачиваний.
docker pull ghcr.io/carbon-witness/graphene-core:1.2.0
docker run -d --name graphene --stop-timeout 300 \
-v graphene-data:/var/lib/graphene -p 1776:1776 -p 127.0.0.1:8090:8090 \
ghcr.io/carbon-witness/graphene-core:1.2.0
Вариант 3. Сборка образа из исходников внутри Docker
Такой же образ, но собранный у себя в докере из клона репозитория, который можно проверить или изменить. На хосте нужен только Docker 23 или новее: все файлы репозитория остаются внутри сборки.
git clone --recurse-submodules -b graphene https://github.com/carbon-witness/graphene-core.git
cd graphene-core
docker build -t graphene-core .
docker run -d --name graphene --stop-timeout 300 \
-v graphene-data:/var/lib/graphene -p 1776:1776 -p 127.0.0.1:8090:8090 \
graphene-core
Каждому процессу компилятора нужно 1,5–2 ГБ памяти, а сборка запускает по одному процессу на ядро. На машине с небольшим объёмом памяти добавьте --build-arg JOBS=2. На двух ядрах сборка идёт около двух часов.
Вместо двух последних команд можно выполнить docker compose up -d --build: образ соберётся, и нода запустится с настройками из compose.yml.
Windows (Docker Desktop)
Проще всего запустить ноду командой в PowerShell: контейнер появится в Docker Desktop, и дальше им можно управлять оттуда.
docker pull carbonwitness/graphene-core:latest
docker run -d --name graphene --stop-timeout 300 `
-v graphene-data:/var/lib/graphene `
-p 1776:1776 -p 127.0.0.1:8090:8090 `
carbonwitness/graphene-core:latest
Таймаут остановки запоминается в контейнере, поэтому кнопка Stop в Docker Desktop тоже будет ждать до 5 минут.
Если удобнее через окно Docker Desktop:
- Поиск вверху окна →
carbonwitness/graphene-core→ тегlatest→ Pull (или в PowerShelldocker pull carbonwitness/graphene-core:latest). - Volumes → Create, имя
graphene-data. - Images →
carbonwitness/graphene-core:latest→ Run → Optional settings:- Container name:
graphene; - Ports:
1776→1776(P2P) и, если нужен RPC,8090→8090; - Volumes: Host path
graphene-data, Container path/var/lib/graphene; - Environment variables – по желанию, например
GRAPHENED_PLUGINS.
- Container name:
В окне Run нет настройки таймаута остановки: кнопка Stop убьёт ноду через 10 секунд, и при следующем запуске может понадобиться полный replay. Кроме того, RPC там открывается на всех интерфейсах, а не только на localhost. Для постоянной работы лучше команда в PowerShell.
Кошелёк из PowerShell:
docker exec -it -u graphene graphene cli_wallet -s ws://127.0.0.1:8090
Что важно знать:
- Таймаут остановки. Нода сбрасывает базу на диск только при штатном выходе. По умолчанию
docker stopждёт 10 секунд и убивает процесс, после чего нужен полный replay. Поэтому--stop-timeout 300, вcompose.yml–stop_grace_period: 5m. - Данные. Цепочка,
config.iniи логи лежат в/var/lib/graphene. Сюда нужно смонтировать том или папку хоста, иначе данные пропадут вместе с контейнером. - Конфиг. При первом старте нода сама пишет
config.iniпо умолчанию. Правите его в томе и перезапускайте контейнер. Старый образ при каждом старте затирал пользовательский конфиг, новый этого не делает. - RPC. В примере порт 8090 открыт только на localhost.
- Переменные
GRAPHENED_*из старого образа сохранены: сиды, плагины, witness ID и так далее.
Аргументы после имени образа уходят в witness_node. Команда без дефиса запускается как программа, например кошелёк:
docker run --rm carbonwitness/graphene-core:1.2.0 --version
docker exec -it -u graphene graphene cli_wallet -s ws://127.0.0.1:8090
Что внутри
Старый Dockerfile достался нам от BitShares и фактически был не работоспособен. Теперь он переписан целиком.
- Сборка в две стадии на Ubuntu 26.04 с тем же тулчейном, что и у версии 1.1: GCC 15, Boost 1.90, OpenSSL 3.5.
- В финальный образ попадают только
witness_node,cli_wallet,get_dev_keyи нужные им библиотеки. Размер – 254 МБ на диске и 67 МБ при скачивании. - Бинарники в образе урезаны: из них убрана отладочная информация, чтобы образ весил меньше. Она не выброшена, а сохранена отдельным архивом в релизе на GitHub. Если нода упадёт, в логе будут только адреса в памяти; с этим архивом их можно превратить в имена функций и номера строк исходного кода. Отдельная отладочная сборка для этого не нужна.
--versionв контейнере показывает хеш коммита, из которого собран образ.- Нода работает не от root, а от пользователя с uid 10001.
Исправление прав в rc2
Первую сборку образа (rc1) сразу проверили на реальной задаче: запустили в докере на нашей API-ноде. На ней тестировались также служебные команды, например управление подключениями к другим нодам.
По умолчанию они закрыты для всех, поэтому был создан файл api-access.json: в нём задаются пользователи, их пароли и доступные им команды. Файл подключали в контейнер и передавали ноде аргументом --api-access /etc/graphene/api-access.json. На хост-машине такой файл мог читать только root.
Поскольку нода в контейнере работает не от root, а от обычного пользователя, такой файл прочитать она не могла. Падала при старте, Docker её перезапускал, и так по кругу. В логе при этом не было ни имени файла, ни слов «нет доступа».
Обойти это можно было и без правок образа – положить файл непосредственно в докер-контейнер. Но подключать секреты с хоста - обычная практика, и контейнер должен справляться с этим сам.
Изнутри контейнера это было не исправить: смонтированный файл сохраняет владельца и права хоста. Поэтому в rc2 было принято решение сделать так же, как в официальных образах postgres и redis:
- контейнер стартует от root;
- свежую папку данных отдаёт пользователю ноды;
- файлы из аргументов, которые нода не может прочитать, копирует внутрь контейнера с нужными правами;
- затем понижает привилегии и запускает
witness_nodeот uid 10001.
Скрипт запуска не остаётся висеть посредником: он уступает место самой ноде. Поэтому команда docker stop доходит прямо до witness_node, та успевает сохранить базу и завершиться штатно. Если бы команду получал скрипт, нода могла бы не узнать об остановке, Docker прибил бы её по таймауту, и при следующем запуске ей пришлось бы проверять всю цепочку.
Если запустить контейнер с --user, всё работает как раньше.
Проверка
- Полная синхронизация с нуля. Образ с Docker Hub на VPS с 2 vCPU дошёл до блока 55 849 207 за 7 ч 16 мин, работая параллельно с боевой нодой. Нативная нода на той же машине синхронизировалась около 11 ч, так что работа в контейнере медленнее не стала.
- Скорость ровная, около 2000 блоков в секунду. База заняла 9 ГБ.
- Остановка на полной базе:
docker stopзанял 4 секунды, нода вышла штатно. - Рестарт: нода поднялась за 2 секунды и доиграла 7 обратимых блоков, без полного replay.
Автоматическая сборка и публикация (CI/CD)
Образ собирается в GitHub Actions:
- при каждом push и PR – сборка и быстрая проверка, без публикации;
- при теге версии – публикация в Docker Hub и GHCR с тегами версии релиза и
latest, плюс черновик релиза с отладочными символами; - при теге релиз-кандидата - то же, но без
latest. - Обновление версии докер-образа на вашей машине - вручную.
Благодаря кешу компилятора повторные сборки занимают около 7 минут вместо 25.
Компоненты
- carbon-witness/graphene-core – ветка
graphene, тегgraphene-1.2.0 - carbon-witness/graphene-fc – ветка
graphene, коммит551377b - carbon-witness/websocketpp – ветка
graphene, коммит571a7b0 - carbon-witness/editline – ветка
graphene, коммит224e256 - carbon-witness/secp256k1-zkp – ветка
graphene, коммитbd06794
Был добавлен форк secp256k1-zkp – криптографическая библиотека для эллиптической кривой secp256k1, той же, что в Bitcoin. Приставка zkp означает расширения для доказательств с нулевым разглашением. Её использует библиотека fc: через неё нода и кошелёк подписывают и проверяют транзакции, а также строят доказательства для слепых (конфиденциальных) переводов. В репозитории fc она подключена сабмодулем vendor/secp256k1-zkp.
Сейчас этот сабмодуль смотрит прямо в репозиторий BitShares. Все остальные зависимости (fc, websocketpp, editline) у graphene-blockchain уже имеют свои форки. Собственный форк secp256k1-zkp убирает последнюю внешнюю зависимость: если BitShares удалит или перепишет свой репозиторий, сборка Graphene не сломается. Код библиотеки после форка не изменялся.
websocketpp, editline и secp256k1-zkp с версии 1.1 не менялись, только ссылки на них - теперь они ведут на форки carbon-witness.
Коммиты
graphene-core
172f6006net: keep peers.json when the peer database is closed twice – база пиров больше не стирается при остановке ноды, плюс тест на это (баг 1)7f180caaapp: announce the P2P node as Graphene, not BitShares – в P2P-сети нода представляется как Graphene, а не BitShares71c789b0version: print 1.2.0 and a version-hash build string – номер версии задан в коде,--versionпечатает1.2.0и хеш коммита (баг 2)26965296fc: bump to TLS 1.2+, masked client frames and the git hash fix – подключает в core исправленную версию fc: TLS, маскирование кадров и хеш коммитаba391107app: connect to --seed-node peers after the P2P node is running – нода подключается к сидам только после запуска своей P2P-части, без паузы в 30 секунд99037764tests: make app_test two_node_network deterministic – тест сети из двух нод больше не падает через разa097bcd4build: point the fc submodule at the carbon-witness fork – сабмодуль fc берётся из нашего форка, где лежат исправленияc1b5151cdocker: rebuild the image on Ubuntu 26.04 – Dockerfile, compose.yml и скрипт запуска переписаны заново под Ubuntu 26.04eb0d3f70ci: build the Docker image and publish it on release tags – автоматическая сборка образа в GitHub Actions и публикация в Docker Hub и GHCR по тегу версии4df66b71ci: move the workflow actions to their Node 24 releases – шаги сборки переведены на актуальные версии, без предупреждений об устаревании78b4229aci: keep the compiler cache between workflow runs – кеш компилятора сохраняется между сборками: сборка занимает около 7 минут вместо 258798f8b8ci: do not tag pre-releases as latest – релиз-кандидаты не получают тегlatest, он остаётся за финальными версиями8a4c1123docker: start as root, fix permissions, drop to uid 10001 – исправление прав в rc2: контейнер сам подготавливает файлы и папку данных и запускает ноду от обычного пользователя949b5787fc: bump to websocketpp and editline on the carbon-witness forks – websocketpp и editline берутся из наших форков82ce9145fc: bump to secp256k1-zkp on the carbon-witness fork – secp256k1-zkp берётся из нашего форка5990f378egenesis: drop the dead seed nodes, add three live ones – обновлён список сидов: 11 мёртвых удалены, 3 живых добавлены- README: правки в readme (
f8091fc1,ec86b97f,1b970896,254a82a0,a3934db7,9f99aef5,66355c45) - Релиз-ноты 1.2.0 (
0a5560c2,bff0234c,d5a98f9a,a24b9060,3e77f051,b1d1c9ea,9c855987)
graphene-fc
85f5d52websocket: TLS 1.2+ and masked client frames –cli_walletснова подключается поwss://, а сетевые кадры клиента уходят со случайной маской (баги 3 и 4)290808dcmake: take the HEAD hash from git rev-parse – в строке версии всегда хеш коммита, а не имя веткиf516383build: point websocketpp and editline at the carbon-witness forks – сабмодули websocketpp и editline переключены на наши форки551377bbuild: point secp256k1-zkp at the carbon-witness fork – сабмодуль secp256k1-zkp переключён на наш форк
Что дальше
Версия 1.2.0 выпущена: образы 1.2.0 и latest опубликованы в Docker Hub и GitHub Container Registry. Следующий шаг – pull request’ы в основной репозиторий graphene-blockchain: сначала fc, потом core.
Дальше по плану из прошлой статьи: сборка под Windows, обновление веб-интерфейса и мобильное приложение.
Спасибо за внимание! Буду рад вашим тестам и отзывам: о найденных проблемах пишите в комментариях или в issues на GitHub.
Работа проводилась при содействии Claude Code.





