feat(chain): v5.3.1 — onblock skip + non-zero mroot bypass + builtin registry (#7)
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>
This commit is contained in:
+1
-1
@@ -15,7 +15,7 @@ set( CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(VERSION_MAJOR 5)
|
||||
set(VERSION_MINOR 3)
|
||||
set(VERSION_PATCH 0)
|
||||
set(VERSION_PATCH 1)
|
||||
set(VERSION_SUFFIX "" CACHE STRING "Optional pre-release suffix appended to version (e.g. rc1, beta). Empty = clean release.")
|
||||
|
||||
if(VERSION_SUFFIX)
|
||||
|
||||
@@ -172,6 +172,19 @@ v5.1.0-abc123def456...
|
||||
|
||||
Ожидаемый `chain_id` основной сети: `6e37f9ac0f0ea717bfdbf57d1dd5d7f0e2d773227d9659a63bbf86eec0326c1b`.
|
||||
|
||||
### Полный ресинк mainnet — встроенные исторические исключения
|
||||
|
||||
Начиная с **v5.3.1** реестр исторических исключений (`chain_historical_exceptions`) поставляется *внутри бинаря* для известных production-цепочек. Для основной сети Кооперативной Экономики (chain_id `6e37f9ac…26c1b`) встроены два окна вокруг инцидента 2026-05-11 (релиз `v5.2.0-dev-294edf3b8`):
|
||||
|
||||
- `action_mroot_zero_windows = [113273322 .. 113275716]` — bypass расхождения action_mroot в окне;
|
||||
- `onblock_skip_windows = [113273322 .. 113275716]` — пропуск implicit-транзакции `onblock`, чтобы state нашего реплея не разъехался с канонической цепью.
|
||||
|
||||
Дополнительно — в `apply_block` снято строгое требование `action_mroot == 0` в bypass'е: внутри окна допускается *любое* расхождение action_mroot при условии полного совпадения остальных полей заголовка.
|
||||
|
||||
Никакого внешнего JSON-файла для mainnet больше не требуется. Достаточно установить `.deb` и запустить полный ресинк — bypass применится автоматически по совпадению `chain_id`. Форки и тестнеты с другим chain_id видят пустой реестр и работают идентично апстрим-Antelope.
|
||||
|
||||
Для разовой переопределения (например, расследование на одной ноде) ещё работает старая опция `chain-historical-exceptions = /path/to/file.json` — она полностью заменяет встроенную запись на этой ноде. JSON формата v5.3.0 (только `action_mroot_zero_windows`) грузится в v5.3.1 без правок.
|
||||
|
||||
### Первый запуск с genesis (полный ресинк с блока 1)
|
||||
|
||||
`--genesis-json` принимается nodeos **только если `data-dir` пуст** (отсутствуют каталоги `state`, `blocks`, `state-history`, `snapshots`). Поэтому при первом запуске или при намеренном пересинке:
|
||||
|
||||
@@ -101,6 +101,7 @@ add_library( eosio_chain
|
||||
eosio_contract_abi_bin.cpp
|
||||
chain_config.cpp
|
||||
chain_id_type.cpp
|
||||
chain_exceptions_builtin.cpp
|
||||
genesis_state.cpp
|
||||
${CMAKE_CURRENT_BINARY_DIR}/genesis_state_root_key.cpp
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#include <eosio/chain/chain_exceptions.hpp>
|
||||
|
||||
namespace eosio { namespace chain {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Compile-time registry of known historical exceptions, keyed by chain_id.
|
||||
*
|
||||
* Add a new entry here when a fix-binary release of coopos must, on a
|
||||
* specific production chain, bypass a permanent inconsistency in the
|
||||
* irreversible block log left behind by an earlier broken producer build.
|
||||
* Each entry is scoped strictly to its chain_id; forks/testnets/devnets with
|
||||
* a different chain_id are unaffected.
|
||||
*
|
||||
* The intent of an entry is to ship enough information in the binary that
|
||||
* any node operator on the named chain can simply install the .deb and run
|
||||
* a full-history replay — no extra config files, no out-of-band JSON.
|
||||
*
|
||||
* To override a builtin entry on a specific node (e.g. during forensic
|
||||
* investigation) set `chain-historical-exceptions` in config.ini to a JSON
|
||||
* file with the matching chain_id; the file replaces the builtin record
|
||||
* entirely for that node.
|
||||
*/
|
||||
const std::vector<chain_historical_exceptions> _builtin_registry = [] {
|
||||
std::vector<chain_historical_exceptions> v;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Coopenomics mainnet (chain_id 6e37f9ac…26c1b)
|
||||
//
|
||||
// 2026-05-11 ~10:18 UTC: the BP deployed `coopos v5.2.0-dev-294edf3b8`
|
||||
// which had two latent defects relative to v5.2.0 release:
|
||||
// - intrinsic table for `assert_recover_key_account` was inserted in
|
||||
// the middle of the table, shifting the IDs of every later intrinsic
|
||||
// (fixed in 5bb051f9a "переместить в конец таблиц — ABI compat");
|
||||
// - the on_activation handler for ASSERT_RECOVER_KEY_ACCOUNT was not
|
||||
// registered (fixed in 2c23b8108).
|
||||
// While that build was producing, the implicit onblock action emitted
|
||||
// no receipts (`_action_receipt_digests` stayed empty) for 2395 blocks,
|
||||
// finalizing `action_mroot = 0`. The BP restarted onto a fixed v5.2.0
|
||||
// build at ~10:43:39 UTC; block 113275717 is the first post-recovery
|
||||
// block with a non-zero action_mroot. ASSERT_RECOVER_KEY_ACCOUNT itself
|
||||
// was only formally activated much later, at block 113318028 (16:36 UTC).
|
||||
//
|
||||
// For a sound replay against this chain we need to:
|
||||
// (a) accept zero-mroot blocks in [113273322..113275716] even though
|
||||
// our build would compute a non-zero local mroot, because the
|
||||
// canonical chain stores zero;
|
||||
// (b) skip onblock execution in that same window so we do NOT mutate
|
||||
// chainbase (e.g. block_summary_object) for those 2395 blocks —
|
||||
// the canonical chain skipped those mutations and our local state
|
||||
// must match for downstream blocks to validate.
|
||||
// No handler suppression is needed: the activation handler that adds
|
||||
// `assert_recover_key_account` to whitelisted_intrinsics runs at block
|
||||
// 113318028, by which time the BP was already on the fixed binary, so
|
||||
// the canonical chain and a v5.3.1 replay agree from that block onward.
|
||||
// -----------------------------------------------------------------------
|
||||
{
|
||||
chain_historical_exceptions e;
|
||||
e.chain_id = chain_id_type(
|
||||
std::string("6e37f9ac0f0ea717bfdbf57d1dd5d7f0e2d773227d9659a63bbf86eec0326c1b") );
|
||||
|
||||
e.action_mroot_zero_windows.push_back({
|
||||
/* from_block */ 113273322u,
|
||||
/* to_block */ 113275716u,
|
||||
/* reason */
|
||||
"Coopenomics mainnet: 2395 irreversible blocks finalized with empty "
|
||||
"_action_receipt_digests (action_mroot = 0) by v5.2.0-dev-294edf3b8 "
|
||||
"between 2026-05-11 10:18 and 10:43 UTC; window boundaries verified "
|
||||
"via leap-util block-log print-log on archived blocks.log."
|
||||
});
|
||||
|
||||
e.onblock_skip_windows.push_back({
|
||||
/* from_block */ 113273322u,
|
||||
/* to_block */ 113275716u,
|
||||
/* reason */
|
||||
"Coopenomics mainnet: onblock implicit action did not record receipts "
|
||||
"on the buggy BP build (mis-ordered intrinsic table), so the canonical "
|
||||
"chain skipped onblock side-effects for these 2395 blocks. A correct "
|
||||
"replay must skip onblock here too, otherwise chainbase mutations "
|
||||
"diverge from canonical state and every later block fails to validate."
|
||||
});
|
||||
|
||||
v.push_back( std::move(e) );
|
||||
}
|
||||
|
||||
return v;
|
||||
}();
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
const std::vector<chain_historical_exceptions>& get_builtin_historical_exceptions() {
|
||||
return _builtin_registry;
|
||||
}
|
||||
|
||||
} } // namespace eosio::chain
|
||||
@@ -701,32 +701,69 @@ struct controller_impl {
|
||||
*/
|
||||
void load_historical_exceptions() {
|
||||
const auto& p = conf.chain_historical_exceptions_path;
|
||||
if( p.empty() ) return;
|
||||
if( !std::filesystem::exists( p ) ) {
|
||||
wlog( "chain-historical-exceptions path ${p} does not exist; running in strict mode",
|
||||
("p", p.string()) );
|
||||
return;
|
||||
}
|
||||
|
||||
chain_historical_exceptions ex;
|
||||
try {
|
||||
ex = fc::json::from_file( p ).as<chain_historical_exceptions>();
|
||||
} catch( const fc::exception& e ) {
|
||||
elog( "failed to parse chain-historical-exceptions file ${p}: ${err}",
|
||||
("p", p.string())("err", e.to_detail_string()) );
|
||||
throw;
|
||||
bool from_file = false;
|
||||
|
||||
if( !p.empty() ) {
|
||||
if( !std::filesystem::exists( p ) ) {
|
||||
wlog( "chain-historical-exceptions path ${p} does not exist; falling back to compiled-in registry",
|
||||
("p", p.string()) );
|
||||
} else {
|
||||
try {
|
||||
ex = fc::json::from_file( p ).as<chain_historical_exceptions>();
|
||||
} catch( const fc::exception& e ) {
|
||||
elog( "failed to parse chain-historical-exceptions file ${p}: ${err}",
|
||||
("p", p.string())("err", e.to_detail_string()) );
|
||||
throw;
|
||||
}
|
||||
EOS_ASSERT( ex.chain_id == chain_id, chain_id_type_exception,
|
||||
"chain-historical-exceptions chain_id (${ex}) does not match running chain (${rc})",
|
||||
("ex", ex.chain_id)("rc", chain_id) );
|
||||
from_file = true;
|
||||
ilog( "loaded chain historical exceptions from file ${p}", ("p", p.string()) );
|
||||
}
|
||||
}
|
||||
|
||||
EOS_ASSERT( ex.chain_id == chain_id, chain_id_type_exception,
|
||||
"chain-historical-exceptions chain_id (${ex}) does not match running chain (${rc})",
|
||||
("ex", ex.chain_id)("rc", chain_id) );
|
||||
if( !from_file ) {
|
||||
// Builtin fallback: match by chain_id.
|
||||
for( const auto& b : get_builtin_historical_exceptions() ) {
|
||||
if( b.chain_id == chain_id ) {
|
||||
ex = b;
|
||||
ilog( "applying compiled-in chain historical exceptions for chain ${cid}",
|
||||
("cid", chain_id) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
historical_exceptions = std::move( ex );
|
||||
|
||||
for( const auto& w : historical_exceptions.action_mroot_zero_windows ) {
|
||||
wlog( "loaded historical action_mroot=0 exception window [${a}..${b}]: ${r}",
|
||||
wlog( "historical action_mroot bypass window [${a}..${b}]: ${r}",
|
||||
("a", w.from_block)("b", w.to_block)("r", w.reason) );
|
||||
}
|
||||
for( const auto& w : historical_exceptions.onblock_skip_windows ) {
|
||||
wlog( "historical onblock skip window [${a}..${b}]: ${r}",
|
||||
("a", w.from_block)("b", w.to_block)("r", w.reason) );
|
||||
}
|
||||
for( const auto& s : historical_exceptions.suppressed_activations ) {
|
||||
wlog( "historical on_activation suppression for feature ${d}: ${r}",
|
||||
("d", s.feature_digest)("r", s.reason) );
|
||||
}
|
||||
}
|
||||
|
||||
bool is_onblock_skipped_for_block( uint32_t block_num ) const {
|
||||
for( const auto& w : historical_exceptions.onblock_skip_windows ) {
|
||||
if( block_num >= w.from_block && block_num <= w.to_block ) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const historical_suppressed_activation* find_suppressed_activation( const digest_type& d ) const {
|
||||
for( const auto& s : historical_exceptions.suppressed_activations ) {
|
||||
if( s.feature_digest == d ) return &s;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static auto validate_db_version( const chainbase::database& db ) {
|
||||
@@ -1815,7 +1852,12 @@ struct controller_impl {
|
||||
}
|
||||
|
||||
if( f.builtin_feature ) {
|
||||
trigger_activation_handler( *f.builtin_feature );
|
||||
if( const auto* suppress = find_suppressed_activation( feature_digest ) ) {
|
||||
wlog( "historical exception: suppressing on_activation handler for feature ${d} at block ${bn}: ${r}",
|
||||
("d", feature_digest)("bn", pbhs.block_num)("r", suppress->reason) );
|
||||
} else {
|
||||
trigger_activation_handler( *f.builtin_feature );
|
||||
}
|
||||
}
|
||||
|
||||
protocol_features.activate_feature( feature_digest, pbhs.block_num );
|
||||
@@ -1867,6 +1909,10 @@ struct controller_impl {
|
||||
});
|
||||
}
|
||||
|
||||
const uint32_t next_block_num = head->block_num + 1;
|
||||
if( is_onblock_skipped_for_block( next_block_num ) ) {
|
||||
wlog( "historical exception: skipping onblock implicit transaction for block ${bn}", ("bn", next_block_num) );
|
||||
} else {
|
||||
try {
|
||||
transaction_metadata_ptr onbtrx =
|
||||
transaction_metadata::create_no_recover_keys( std::make_shared<packed_transaction>( get_on_block_transaction() ),
|
||||
@@ -1895,6 +1941,7 @@ struct controller_impl {
|
||||
} catch( ... ) {
|
||||
elog( "on block transaction failed due to unknown exception" );
|
||||
}
|
||||
}
|
||||
|
||||
clear_expired_input_transactions(deadline);
|
||||
update_producers_authority();
|
||||
@@ -2213,15 +2260,23 @@ struct controller_impl {
|
||||
|
||||
if( producer_block_id != ab._id ) {
|
||||
bool historical_bypass = false;
|
||||
if( !historical_exceptions.action_mroot_zero_windows.empty()
|
||||
&& b->action_mroot == digest_type() )
|
||||
{
|
||||
if( !historical_exceptions.action_mroot_zero_windows.empty() ) {
|
||||
const uint32_t bn = b->block_num();
|
||||
for( const auto& w : historical_exceptions.action_mroot_zero_windows ) {
|
||||
if( bn < w.from_block || bn > w.to_block ) continue;
|
||||
// Bypass demands that EVERY block header field other than
|
||||
// action_mroot matches what the producer recorded. The
|
||||
// canonical chain may carry either a zero action_mroot
|
||||
// (broken build emitted empty digests) or a non-zero one
|
||||
// that simply diverges from what a sound replay computes;
|
||||
// in either case the only allowed difference within a
|
||||
// declared window is action_mroot itself.
|
||||
if( !other_header_fields_match( *b, *ab._unsigned_block ) ) continue;
|
||||
wlog( "historical action_mroot=0 exception applied for block ${bn} in window [${a}..${c}]: ${r}",
|
||||
("bn", bn)("a", w.from_block)("c", w.to_block)("r", w.reason) );
|
||||
wlog( "historical action_mroot bypass applied for block ${bn} in window [${a}..${c}] "
|
||||
"(producer mroot ${pm}, local mroot ${lm}): ${r}",
|
||||
("bn", bn)("a", w.from_block)("c", w.to_block)
|
||||
("pm", b->action_mroot)("lm", ab._unsigned_block->action_mroot)
|
||||
("r", w.reason) );
|
||||
historical_bypass = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
namespace eosio { namespace chain {
|
||||
|
||||
/**
|
||||
* A single window of block numbers in which `action_mroot` is allowed to be
|
||||
* the zero digest. Used to recover full-history replay past historical
|
||||
* incidents where a buggy producer build emitted blocks with zero action_mroot
|
||||
* that are now permanently part of the irreversible chain.
|
||||
* A single window of block numbers in which `action_mroot` is allowed to
|
||||
* diverge from the locally-computed value. Used to recover full-history
|
||||
* replay past historical incidents where a buggy producer build emitted
|
||||
* blocks whose action_mroot is permanently part of the irreversible chain
|
||||
* yet cannot be reproduced by a sound replay.
|
||||
*/
|
||||
struct historical_action_mroot_window {
|
||||
uint32_t from_block = 0;
|
||||
@@ -20,10 +21,40 @@ struct historical_action_mroot_window {
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-chain set of historical validation exceptions. Loaded from an external
|
||||
* JSON file at startup; the file must declare the `chain_id` of the chain it
|
||||
* targets, and the controller refuses to load it for any other chain. Absent
|
||||
* file => strict upstream validation, no bypass.
|
||||
* A window in which the implicit `onblock` transaction must be skipped during
|
||||
* apply_block. Use when the producer build of record failed to record onblock
|
||||
* receipts: re-executing onblock during replay would mutate state (e.g.
|
||||
* block_summary_object) inconsistently with the canonical chain and cause
|
||||
* permanent divergence from this window onward.
|
||||
*/
|
||||
struct historical_onblock_skip_window {
|
||||
uint32_t from_block = 0;
|
||||
uint32_t to_block = 0;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
/**
|
||||
* Suppress the on_activation handler for a specific protocol feature digest.
|
||||
* Use when the producer build of record marked a feature as activated but
|
||||
* failed to run its activation handler — re-running the handler during replay
|
||||
* mutates chainbase (e.g. whitelisted_intrinsics in protocol_state_object)
|
||||
* differently from the canonical chain.
|
||||
*/
|
||||
struct historical_suppressed_activation {
|
||||
digest_type feature_digest;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-chain set of historical validation exceptions. May be loaded from an
|
||||
* external JSON file at startup, OR matched from a compiled-in registry
|
||||
* keyed by `chain_id`. The controller refuses to load a file whose
|
||||
* `chain_id` does not match the running chain. Absent file AND no builtin
|
||||
* match => strict upstream validation, no bypass.
|
||||
*
|
||||
* Fields beyond chain_id are optional in the JSON form — older files
|
||||
* declaring only action_mroot_zero_windows continue to parse cleanly into
|
||||
* a record whose new vectors are empty.
|
||||
*/
|
||||
struct chain_historical_exceptions {
|
||||
// chain_id_type's default ctor is private upstream; provide an explicit
|
||||
@@ -32,14 +63,36 @@ struct chain_historical_exceptions {
|
||||
chain_historical_exceptions()
|
||||
: chain_id( chain_id_type::empty_chain_id() ) {}
|
||||
|
||||
chain_id_type chain_id;
|
||||
std::vector<historical_action_mroot_window> action_mroot_zero_windows;
|
||||
chain_id_type chain_id;
|
||||
std::vector<historical_action_mroot_window> action_mroot_zero_windows;
|
||||
std::vector<historical_onblock_skip_window> onblock_skip_windows;
|
||||
std::vector<historical_suppressed_activation> suppressed_activations;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compiled-in registry of historical exceptions, keyed by chain_id. Empty
|
||||
* unless `coopos` ships with known mainnet incidents baked in. The loader
|
||||
* consults this list when no external file is configured: any entry whose
|
||||
* chain_id equals the running controller's chain_id is applied verbatim,
|
||||
* so archival nodes on known chains work out of the box without an extra
|
||||
* config artifact. Forks/testnets with a different chain_id see no entry
|
||||
* and behave like upstream Antelope.
|
||||
*/
|
||||
const std::vector<chain_historical_exceptions>& get_builtin_historical_exceptions();
|
||||
|
||||
} } // namespace eosio::chain
|
||||
|
||||
FC_REFLECT( eosio::chain::historical_action_mroot_window,
|
||||
(from_block)(to_block)(reason) )
|
||||
|
||||
FC_REFLECT( eosio::chain::historical_onblock_skip_window,
|
||||
(from_block)(to_block)(reason) )
|
||||
|
||||
FC_REFLECT( eosio::chain::historical_suppressed_activation,
|
||||
(feature_digest)(reason) )
|
||||
|
||||
FC_REFLECT( eosio::chain::chain_historical_exceptions,
|
||||
(chain_id)(action_mroot_zero_windows) )
|
||||
(chain_id)
|
||||
(action_mroot_zero_windows)
|
||||
(onblock_skip_windows)
|
||||
(suppressed_activations) )
|
||||
|
||||
@@ -4,12 +4,18 @@
|
||||
* block-number windows for a specific chain_id.
|
||||
*
|
||||
* Covered:
|
||||
* - JSON round-trip of the exceptions struct
|
||||
* - chain_id mismatch in the file → controller refuses to start
|
||||
* - block with action_mroot=0 inside declared window → bypass accepts it
|
||||
* - block with action_mroot=0 outside declared window → still rejected
|
||||
* - block with other header field also corrupted → still rejected (bypass
|
||||
* is not a universal amnesty)
|
||||
* - block with NON-zero divergent action_mroot inside window → bypass also
|
||||
* accepts (v5.3.1: dropped the `action_mroot==0` strict guard, because in
|
||||
* practice a sound replay may compute a non-zero mroot inside the window
|
||||
* that simply does not match what the buggy producer recorded)
|
||||
* - chain_historical_exceptions JSON without the new optional fields
|
||||
* (onblock_skip_windows, suppressed_activations) still round-trips
|
||||
* (backward compatibility with files produced for v5.3.0)
|
||||
*
|
||||
* The "no file → strict" baseline is implicit: a default tester carries no
|
||||
* exceptions, and a block with zero'd action_mroot would not pass push_block
|
||||
@@ -218,4 +224,86 @@ BOOST_AUTO_TEST_CASE(in_window_but_other_field_altered_rejected) {
|
||||
fc::exception );
|
||||
}
|
||||
|
||||
// v5.3.1: bypass no longer requires action_mroot == 0. Any divergent mroot
|
||||
// inside a declared window is accepted as long as the rest of the header
|
||||
// matches. Models the real coopenomics mainnet case where 113275717+ have
|
||||
// non-zero but divergent action_mroot for some span.
|
||||
BOOST_AUTO_TEST_CASE(in_window_nonzero_divergent_mroot_bypass) {
|
||||
tester main;
|
||||
main.produce_block();
|
||||
main.create_account( "newacc"_n );
|
||||
auto good_block = main.produce_block();
|
||||
// Replace mroot with arbitrary non-zero digest that the validator will not
|
||||
// reproduce locally.
|
||||
auto divergent = clone_block_with_action_mroot(
|
||||
main, good_block,
|
||||
digest_type::hash( std::string("synthetic-divergent-mroot") ) );
|
||||
|
||||
fc::temp_directory tempdir;
|
||||
auto cfg_pair = base_tester::default_config( tempdir );
|
||||
auto cfg = cfg_pair.first;
|
||||
|
||||
auto ex_path = tempdir.path() / "exceptions-in-window-nonzero.json";
|
||||
write_exceptions_file(
|
||||
ex_path,
|
||||
main.control->get_chain_id(),
|
||||
{ { 1, 1000, "v5.3.1: bypass accepts any divergent mroot in window" } } );
|
||||
cfg.chain_historical_exceptions_path = ex_path;
|
||||
|
||||
tester validator( cfg, cfg_pair.second );
|
||||
for( uint32_t bn = 2; bn < good_block->block_num(); ++bn ) {
|
||||
auto prev = main.control->fetch_block_by_number( bn );
|
||||
auto bsf = validator.control->create_block_state_future( prev->calculate_id(), prev );
|
||||
controller::block_report br;
|
||||
validator.control->push_block( br, bsf.get(), forked_branch_callback{}, trx_meta_cache_lookup{} );
|
||||
}
|
||||
|
||||
auto bsf = validator.control->create_block_state_future( divergent->calculate_id(), divergent );
|
||||
validator.control->abort_block();
|
||||
controller::block_report br;
|
||||
BOOST_REQUIRE_NO_THROW(
|
||||
validator.control->push_block( br, bsf.get(), forked_branch_callback{}, trx_meta_cache_lookup{} ) );
|
||||
}
|
||||
|
||||
// v5.3.1: chain_historical_exceptions struct gained two optional vectors
|
||||
// (onblock_skip_windows, suppressed_activations). A JSON file produced for
|
||||
// v5.3.0 (mentioning only action_mroot_zero_windows) must still parse, with
|
||||
// the new vectors defaulting to empty.
|
||||
BOOST_AUTO_TEST_CASE(legacy_json_without_new_fields_round_trips) {
|
||||
const std::string legacy_json = R"({
|
||||
"chain_id": "6e37f9ac0f0ea717bfdbf57d1dd5d7f0e2d773227d9659a63bbf86eec0326c1b",
|
||||
"action_mroot_zero_windows": [
|
||||
{ "from_block": 1, "to_block": 10, "reason": "legacy v5.3.0 file" }
|
||||
]
|
||||
})";
|
||||
|
||||
chain_historical_exceptions ex;
|
||||
BOOST_REQUIRE_NO_THROW(
|
||||
ex = fc::json::from_string( legacy_json ).as<chain_historical_exceptions>() );
|
||||
BOOST_REQUIRE_EQUAL( ex.action_mroot_zero_windows.size(), 1u );
|
||||
BOOST_REQUIRE_EQUAL( ex.action_mroot_zero_windows[0].from_block, 1u );
|
||||
BOOST_REQUIRE_EQUAL( ex.action_mroot_zero_windows[0].to_block, 10u );
|
||||
BOOST_REQUIRE( ex.onblock_skip_windows.empty() );
|
||||
BOOST_REQUIRE( ex.suppressed_activations.empty() );
|
||||
}
|
||||
|
||||
// v5.3.1: the compiled-in registry is consulted when no file is configured.
|
||||
// It must contain the coopenomics mainnet entry by chain_id.
|
||||
BOOST_AUTO_TEST_CASE(builtin_registry_contains_mainnet_entry) {
|
||||
const auto& reg = get_builtin_historical_exceptions();
|
||||
BOOST_REQUIRE( !reg.empty() );
|
||||
const chain_id_type mainnet(
|
||||
std::string("6e37f9ac0f0ea717bfdbf57d1dd5d7f0e2d773227d9659a63bbf86eec0326c1b") );
|
||||
bool found = false;
|
||||
for( const auto& e : reg ) {
|
||||
if( e.chain_id == mainnet ) {
|
||||
found = true;
|
||||
BOOST_REQUIRE( !e.action_mroot_zero_windows.empty() );
|
||||
BOOST_REQUIRE( !e.onblock_skip_windows.empty() );
|
||||
break;
|
||||
}
|
||||
}
|
||||
BOOST_REQUIRE( found );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
Reference in New Issue
Block a user