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