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>
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>
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.
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).