The genesis registry was a namespace-scope static whose initializer parses
public_key_type values; fc's own static state is not guaranteed to exist at
that point, and every leap-util invocation (even 'version client') aborted
with fc::assert_exception before main() — classic static init order fiasco.
nodeos was unaffected only because the linker never pulls the unused object
file out of the static library.
Both builtin registries (genesis, historical exceptions) now use function-
local statics built on first use.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Bootstrapping a fresh node used to require an externally hosted genesis.json;
lose the file and you cannot sync. The canonical genesis states of known
Coopenomics chains (mainnet, testnet) are now compiled into the binary and
reproducible from the package alone:
leap-util genesis list # names + chain ids
leap-util genesis print mainnet > genesis.json
Each entry only overrides initial_key — the production initial_timestamp and
chain_config values are already the compile-time defaults of genesis_state.
The default EOSIO_ROOT_KEY is intentionally untouched, so dev forks and the
docker-hub harness keep their well-known dev key; one build serves mainnet,
testnet and dev (a config-time root key change would have forked every dev
setup, and shipping per-network builds was rejected outright).
Unit test pins both registry chain ids to the canonical values, proving every
hashed genesis field is correct, and pins the dev default to NOT equal any
production genesis.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Empirical full replay on 2026-06-10 proved the incident model wrong in one
detail: onblock on the buggy build THREW on every block (it did not merely
skip receipt recording), and recovery happened in-band — block 113275717
carries the eosio::setcode that replaced the system contract, produced 0.5s
after the last zero-mroot block with no BP restart. Onblock was still failing
while 113275717 itself was produced, so its canonical action_mroot covers
only the setcode receipt; a replay that runs onblock there computes a
two-receipt mroot and fails "Block ID does not match".
Extend both builtin windows (action_mroot bypass + onblock skip) from
113275716 to 113275717 inclusive. The 2395 in-window blocks replay
bit-identical to the canonical chain (zero mroot, matching block ids — the
mroot bypass never even fires); user transactions inside the window execute
normally.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Three coupled mechanisms in chain_historical_exceptions so archival nodes can
replay coopenomics mainnet past the 2026-05-11 v5.2.0-dev incident:
- chain_historical_exceptions gains onblock_skip_windows and
suppressed_activations vectors; both are optional in the JSON form so v5.3.0
files keep parsing.
- apply_block bypass no longer requires action_mroot == 0 inside a window —
any divergent mroot is accepted when every other header field matches.
Needed because a sound replay computes a non-zero mroot in the dirty window
(onblock works) while the canonical chain stored zero.
- start_block skips the implicit onblock transaction inside any configured
window. Without this the chainbase (block_summary_object etc.) mutated by
onblock diverges from canonical state and every later block fails to
validate, defeating the mroot bypass.
- trigger_activation_handler can be suppressed per feature_digest via the
registry — guards against future incidents where a marked-activated feature
must not run its handler during replay.
- New chain_exceptions_builtin.cpp ships a compiled-in registry keyed by
chain_id; for coopenomics mainnet it carries action_mroot + onblock-skip
windows [113273322..113275716]. Operators no longer need to distribute a
separate JSON file; the .deb is self-contained. File option still wins
per-node when set.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
CMakeLists.txt still hardcoded 5.2.0, so v5.3.0 tag produced
coopos_5.2.0-*.deb. dpkg -i over installed 5.2.0 would no-op.
Bump VERSION_MINOR=3 so artifact matches tag.
The template overload fc::json::to_string<T>(...) in this fork takes the
deadline argument without a default (only the variant overload has a
default), so the one-arg call inside json_roundtrip would not compile:
error: no matching function for call to
'fc::json::to_string(eosio::chain::chain_historical_exceptions&)'
note: candidate expects 4 arguments, 1 provided
JSON round-trip is already exercised indirectly by every remaining test
case — each writes the file via fc::json::to_pretty_string (template
overload with deadline default) and the controller reads it back via
fc::json::from_file<T>() in the loader.
chain_id_type has a private default constructor (only friend classes like
controller_impl and fc::variant::as can default-construct one). Relying on
the implicit default ctor in chain_historical_exceptions therefore made it
not default-constructible, which broke both the controller_impl member init
and fc::variant::as<chain_historical_exceptions>() in the JSON loader:
error: use of deleted function
'eosio::chain::chain_historical_exceptions::chain_historical_exceptions()'
note: 'eosio::chain::chain_id_type::chain_id_type()' is private
Provide an explicit default ctor that initialises chain_id via the public
empty_chain_id() factory. Loader then overwrites it with the value parsed
from the JSON file and asserts equality with self.get_chain_id().
Adds a generic per-chain exception mechanism that lets a controller skip
the producer_block_id != ab._id assert in apply_block when the only
header divergence is a known historical action_mroot=0 inside a declared
block-number window for the matching chain_id.
Motivation: on 2026-05-11 the mainnet Коопеномикс BP briefly ran a dev
build (v5.2.0-dev-294edf3b8) that did not register the on_activation
handler for ASSERT_RECOVER_KEY_ACCOUNT (id=24). For ~20 minutes (blocks
113273322..113275716) onblock did not register intrinsics,
_action_receipt_digests was empty and finalized action_mroot was the
zero digest. The window is now permanently irreversible. Any current
binary refuses to replay past block 113273322 with
block_validate_exception, so new full-history archive nodes / fresh BPs
/ --hard-replay-blockchain on mainnet are blocked.
Design (see exception-notes.md):
- New struct chain_historical_exceptions { chain_id, windows[] }
loaded from a JSON file path supplied via the new config option
chain-historical-exceptions. Absent / empty file => no change in
behavior, strict upstream Antelope validation. Forks and subnets
reusing this codebase without supplying a file are unaffected.
- Loader fires from controller_impl::init() right after
protocol_features.init(db) (where self.get_chain_id() is valid) and
EOS_ASSERTs that the file's chain_id matches the running chain — so
the mainnet exception file cannot accidentally be applied to a
different chain.
- Bypass site is the existing producer_block_id != ab._id branch in
apply_block. Bypass fires only when all three hold: block_num is
inside a declared window, b->action_mroot is strictly the zero
digest, and a new other_header_fields_match helper confirms every
other header field (timestamp, producer, confirmed, previous,
transaction_mroot, schedule_version, new_producers,
header_extensions) matches the locally-assembled block. Any other
header divergence still throws block_validate_exception as before.
- Every bypass application is logged via wlog with the configured
reason for forensics.
The data file for mainnet Коопеномикс is shipped separately (lives in
the playbooks repo, deployed to /etc/coopos/exceptions/) so the data
never leaks into forks that simply pull this codebase.
Unit tests cover JSON round-trip, chain_id mismatch -> startup refusal,
in-window action_mroot=0 -> bypass accepts, out-of-window -> rejected,
and in-window-but-other-field-altered -> rejected.
Refs: incident 2026-05-11, coopos commit 2c23b8108 (root-cause fix that
arrived too late for the dirty window).
Перенёс из общей auto-memory в репо: миграции версий, snapshot-vs-full-resync, Dockerfile.publish target. Часть реорганизации памяти агента по проектам.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previous commit declared template specialization but missed
set_activation_handler<>() call in controller_impl ctor — so the handler
was never inserted into protocol_feature_activation_handlers map and
trigger_activation_handler() returned silently on feature activation.
Result: feature got marked activated in state, but assert_recover_key_account
was not added to whitelisted_intrinsics. Existing chains with the old build
must re-init from a pre-activation snapshot to get the handler to run during
replay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds new builtin protocol feature that whitelists assert_recover_key_account
intrinsic on activation. Required for existing chains (testnet/mainnet) where
this intrinsic is not in whitelisted_intrinsics state — they cannot accept
WASM that imports env.assert_recover_key_account until the feature is
preactivated + activated via eosio::activate.
on_activation handler is idempotent: на свежем genesis intrinsic уже
добавлен через genesis_intrinsics, на действующих цепочках добавляется
здесь.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Добавлен новый раздел "Запуск ноды на основной сети Кооперативной Экономики":
- embedded production genesis.json с initial_key EOS7Tjq... (mainnet chain_id 6e37f9ac...);
- объяснение почему дефолтный EOSIO_ROOT_KEY из бинарника не подходит и нужен явный --genesis-json при первом запуске на пустой data-dir;
- пошаговая процедура полного ресинка через --genesis-json (stop → wipe data → подложить genesis → флаг в unit → start → снять флаг);
- альтернативная процедура быстрого старта через --snapshot.
TOC обновлён (пункт 5).
Раньше каждая сборка получала имя `coopos_5.2.0-dev-...deb` и
`coopos-dev_5.2.0-dev-...deb` — слово «dev» в имени файла означало
одновременно (а) pre-release-суффикс версии и (б) component-имя
второго пакета. Это путало: «у одного dev в начале, у другого
в конце».
VERSION_SUFFIX переведён в CACHE STRING с пустым default. Релизный
билд даёт чистое `5.2.0`. При необходимости pre-release —
`cmake -DVERSION_SUFFIX=rc1 …`.
Имена пакетов после правки:
- coopos_5.2.0-ubuntu22.04_amd64.deb (runtime)
- coopos-dev_5.2.0-ubuntu22.04_amd64.deb (headers, EosioTester)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Вставка нового intrinsic в середину intrinsic_mapping/genesis_intrinsics/eos-vm
регистраций сдвинула индексы всех последующих host functions на 1. Старые wasm
(включая eosio.system, скомпилированный до v5.2.0) по импортам "env.X" попадают
на новые индексы, и context-aware вызов из onblock приземляется на
context-free-only assert_recover_key_account → REJECT каждого блока с
"this API may only be called from context_free apply".
Перенос в конец всех трёх таблиц восстанавливает порядок индексов для ранее
существовавших intrinsics. Сам метод остаётся доступным с новым (большим)
индексом — будущие wasm-сборки увидят его как обычно.
Без `target: runtime` buildx публиковал последнюю стадию `deb`
(scratch с .deb файлами), а не runnable runtime. Из-за этого
dicoop/blockchain:latest и :v5.2.0 на Hub были неисполнимым
dist-артефактом.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cpack on Ubuntu 22.04 produces control.tar.zst, but the leap-era
tools/tweak-deb.sh hardcodes 'ar x control.tar.gz', which fails with
'tar: Unexpected EOF in archive'.
The tweak is only a cosmetic libc-dep cleanup; without it the .deb
files are still valid. Drop the call.
CMakeLists.txt reads LICENSE via configure_file at line 219; excluding
it from .dockerignore caused the build to fail with:
CMake Error: File /blockchain/LICENSE does not exist.
Be conservative — only exclude things definitely unused by cmake:
build artefacts, logs, CI configs, and images/ (logos).
Adds DEB packaging to the same docker-publish workflow:
Builder stage (Dockerfile.publish):
- Pass -DENABLE_LEAP_DEV_DEB=ON to cmake. This activates component-based
install rules and lets cpack -G DEB produce two debs:
* coopos_<ver>-ubuntu22.04_amd64.deb (runtime: nodeos/cleos/...)
* coopos-dev_<ver>-ubuntu22.04_amd64.deb (headers, libs, cmake
config — required to build smart contracts and run mono tests
that depend on find_package(leap))
- Run cpack -G DEB after make install
- Apply tools/tweak-deb.sh to fix overly-strict libc deps in control
- Stage debs at /out/deb/ for the next stage to export
New 'deb' stage (FROM scratch):
- Exports only the .deb files. Used by CI to extract packages without
pulling the full builder image.
Workflow:
- New step runs buildx with --target deb --output type=local to pull
the .deb files onto the runner. This reuses the same GHA cache scope
as the runtime image, so on a tag with cache-hit it adds only minutes.
- On tag pushes, .deb files are attached to the GitHub Release via
softprops/action-gh-release@v2 (tag is created if missing).
- Always upload .deb as a workflow artifact too (30 days retention),
so manual runs still produce downloadable packages.
- permissions: contents: write — needed to write to releases.
- .dockerignore excludes .cicd/, .github/, docs/, tutorials/, images/,
README.md, CONTRIBUTING.md, LICENSE from the coopos build-context.
Changes to CI/docs no longer invalidate the heavy COPY --from=coopos
layer, so subsequent builds become true cache-hits.
- cache-from/to now use scope=docker-publish (instead of default per-ref
scope). GHA cache is reused across different tags (v5.2.0, v5.2.1, …)
and across branches, so a new tag is no longer a cold rebuild.
* ci: add multi-stage Dockerfile.publish for Docker Hub image
Multi-stage Dockerfile that builds blockchain (coopos) and CDT in a
builder stage, then copies only /usr/local into a slim runtime stage.
Will be used by docker-publish workflow to publish
dicoop/blockchain:<tag> on every v* tag.
* ci: add docker-publish workflow
Triggers on v* tags and via workflow_dispatch (cdt-ref/version-tag/
push-latest inputs). Builds with .cicd/Dockerfile.publish on
ubuntu-latest with BuildKit GHA cache, pushes to Docker Hub as
dicoop/blockchain:<tag> and :latest.
---------
Co-authored-by: coopops <coopos@coopenomics.world>