diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5ce67169c8..4062b01c9c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,6 +26,7 @@ jobs: - name: HF16 offline regression checks run: | set -euo pipefail + python3 tests/p2p/test_slot_interval.py python3 tests/p2p/test_vote_ingress.py python3 tests/validator/test_signing_cache.py python3 tests/chain/test_snapshot_restore.py diff --git a/@l10n/ru/docs/consensus/hf16.md b/@l10n/ru/docs/consensus/hf16.md index 820caea937..9697cf80b3 100644 --- a/@l10n/ru/docs/consensus/hf16.md +++ b/@l10n/ru/docs/consensus/hf16.md @@ -46,3 +46,5 @@ HF16 намеренно сохраняет **1.000 VIZ на произведён Параметры хранятся и голосуются в **реальном числе блоков**, согласно выбранной политике миграции HF16. Однократная миграция увеличивает существующие голоса и медиану втрое, но не распознаёт последующий голос из старого скрипта. Делегатам нужно увеличить зашитые `inflation_recalc_period`, `distribution_epoch_length`, `pm_batch_epoch_blocks`, `pm_reveal_window_blocks` втрое для сохранения длительности. Для голосования defaults используйте актуальный `get_config.CHAIN_DEFAULT_CHAIN_PROPERTIES`; при изменении другого поля сохраняйте остальные текущие on-chain значения. Старые defaults структуры повторно не отправляйте. `get_config` согласованно возвращает текущие блоки/час/сутки/год, ёмкость undo, пороги пропусков и defaults. Шаг изменения reserve ratio сохраняет 60 секунд. `CHAIN_EMERGENCY_EXIT_NORMAL_BLOCKS` — неиспользуемая историческая константа: выход из emergency определяется кворумом активных валидаторов, а не таймером 21 блок. + +Пороги P2P-синхронизации (отставание, разрешение форка, wedge watchdog) масштабируются по текущей длине слота и сохраняют прежнюю длительность. Опции оператора в блоках не пересчитываются: `dlt-block-log-max-blocks`, `snapshot-every-n-blocks`, `history-count-blocks`, `block-archive-range` после HF16 покрывают втрое меньший срок. Скользящий block log по умолчанию (100000 блоков) обслуживает синхронизацию пиров около 28 часов вместо 3,5 дней; значение 300000 сохраняет прежнее окно. diff --git a/@l10n/ru/docs/node/snapshot.md b/@l10n/ru/docs/node/snapshot.md index 76a0544829..6d6575d519 100644 --- a/@l10n/ru/docs/node/snapshot.md +++ b/@l10n/ru/docs/node/snapshot.md @@ -102,6 +102,8 @@ snapshot-max-age-days = 90 | Ежедневно | 28 800 | ~24 часа | | Еженедельно | 100 000 | ~3,5 дня | +Время указано для блоков по 3 секунды. После HF16 (блок 1 секунда) то же число блоков покрывает втрое меньший срок; для прежней длительности умножьте значение на три. + ### Метод 4: Совмещение at-block и периодических Оба параметра могут быть активны одновременно: @@ -197,7 +199,7 @@ vizd \ - **API-доступа** — `get_block` работает для блоков в скользящем окне. ```ini -dlt-block-log-max-blocks = 100000 # хранить последние ~3,5 дня блоков +dlt-block-log-max-blocks = 100000 # ~3,5 дня до HF16, ~28 ч после; 300000 сохраняет 3,5 дня ``` Когда лог превышает этот размер, старые блоки удаляются с начала. Реализация отслеживает логические размеры файлов независимо от файла, отображаемого в память, для предотвращения ошибок с устаревшим размером после тысяч циклов изменения размера. @@ -347,7 +349,7 @@ snapshot-every-n-blocks = 28800 snapshot-dir = /data/viz-snapshots snapshot-max-age-days = 90 -# DLT-скользящий block log: хранить последние ~3,5 дня +# DLT-скользящий block log: ~3,5 дня до HF16, ~28 ч после (300000 сохраняет 3,5 дня) dlt-block-log-max-blocks = 100000 shared-file-size = 4G diff --git a/@l10n/zh-CN/docs/node/snapshot.md b/@l10n/zh-CN/docs/node/snapshot.md index bcdb68f9fc..69e83e3974 100644 --- a/@l10n/zh-CN/docs/node/snapshot.md +++ b/@l10n/zh-CN/docs/node/snapshot.md @@ -102,6 +102,8 @@ snapshot-max-age-days = 90 | 每日 | 28,800 | 约 24 小时 | | 每周 | 100,000 | 约 3.5 天 | +以上时间按 3 秒出块计算。HF16 之后(1 秒出块)相同区块数只覆盖三分之一的时间;要保持原时长,请将数值乘以三。 + ### 方法 4:结合 at-block 和周期性 两个设置可同时激活: @@ -197,7 +199,7 @@ DLT 模式下主 `block_log` 保持为空。`dlt_block_log`(文件 `dlt_block_ - **API 访问** — `get_block` 对滚动窗口内的区块有效。 ```ini -dlt-block-log-max-blocks = 100000 # 保留最近约 3.5 天的区块 +dlt-block-log-max-blocks = 100000 # HF16 前约 3.5 天,之后约 28 小时;300000 可保持 3.5 天 ``` 当日志超过此大小时,旧区块从前端修剪。实现独立于内存映射文件跟踪逻辑文件大小,以防止数千次调整大小循环后出现过期大小错误。 @@ -347,7 +349,7 @@ snapshot-every-n-blocks = 28800 snapshot-dir = /data/viz-snapshots snapshot-max-age-days = 90 -# DLT 滚动 block log:保留最近约 3.5 天 +# DLT 滚动 block log:HF16 前约 3.5 天,之后约 28 小时(300000 可保持 3.5 天) dlt-block-log-max-blocks = 100000 shared-file-size = 4G diff --git a/docs/consensus/hf16.md b/docs/consensus/hf16.md index 01de3c6c4a..da03e5e81d 100644 --- a/docs/consensus/hf16.md +++ b/docs/consensus/hf16.md @@ -46,3 +46,5 @@ Vote/equivocation history retains its one-hour wall-time window: `3600 / block_i Stored and voted properties remain **native block counts**, per the selected HF16 migration policy. The one-time migration scales existing votes and the median by three; it cannot identify a later vote copied from an old script. Validators must update hard-coded `inflation_recalc_period`, `distribution_epoch_length`, `pm_batch_epoch_blocks` and `pm_reveal_window_blocks` by three to preserve time. Alternatively, use live `get_config.CHAIN_DEFAULT_CHAIN_PROPERTIES` when intentionally voting defaults, or preserve the current on-chain properties when editing unrelated fields. Do not re-vote stale struct defaults. `get_config` reports consistent live blocks/hour/day/year, undo capacity, missed-block thresholds and default properties. The reserve-ratio adjustment retains its 60-second cadence. `CHAIN_EMERGENCY_EXIT_NORMAL_BLOCKS` is an unused legacy constant; actual emergency exit depends on the active-validator schedule quorum, not a 21-block timer. + +P2P sync thresholds (fall-behind, fork resolution, wedge watchdog) are scaled by the live slot length, so they keep their wall-clock meaning. Operator options denominated in blocks are not rescaled: `dlt-block-log-max-blocks`, `snapshot-every-n-blocks`, `history-count-blocks`, `block-archive-range` cover one third of the previous time after HF16. The default rolling block log of 100000 blocks serves peer sync for about 28 hours instead of 3.5 days; set 300000 to keep the old window. diff --git a/docs/node/snapshot.md b/docs/node/snapshot.md index 44ca0fbebd..d05eff483f 100644 --- a/docs/node/snapshot.md +++ b/docs/node/snapshot.md @@ -102,6 +102,8 @@ Block processing is paused only during Phase 1; API and P2P reads are unaffected | Daily | 28,800 | ~24 hours | | Weekly | 100,000 | ~3.5 days | +Times assume 3-second blocks. After HF16 (1-second blocks) the same block count covers one third of the time; multiply by three to keep the duration. + ### Method 4: Combining at-block and periodic Both settings can be active simultaneously: @@ -197,7 +199,7 @@ In DLT mode the main `block_log` stays empty. The `dlt_block_log` (files `dlt_bl - **API access** — `get_block` works for blocks in the rolling window. ```ini -dlt-block-log-max-blocks = 100000 # keep last ~3.5 days of blocks +dlt-block-log-max-blocks = 100000 # ~3.5 days before HF16, ~28 h after; use 300000 to keep 3.5 days ``` When the log exceeds this size, old blocks are pruned from the front. The implementation tracks logical file sizes independently of the memory-mapped file to prevent stale-size bugs after thousands of resize cycles. @@ -347,7 +349,7 @@ snapshot-every-n-blocks = 28800 snapshot-dir = /data/viz-snapshots snapshot-max-age-days = 90 -# DLT rolling block log: keep last ~3.5 days +# DLT rolling block log: ~3.5 days before HF16, ~28 h after (300000 keeps 3.5 days) dlt-block-log-max-blocks = 100000 shared-file-size = 4G diff --git a/libraries/network/dlt_p2p_node.cpp b/libraries/network/dlt_p2p_node.cpp index c825e93efb..e0d3fef708 100644 --- a/libraries/network/dlt_p2p_node.cpp +++ b/libraries/network/dlt_p2p_node.cpp @@ -60,6 +60,10 @@ dlt_p2p_node::~dlt_p2p_node() { // ── Configuration ──────────────────────────────────────────────────── void dlt_p2p_node::set_delegate(dlt_p2p_delegate* del) { _delegate = del; } +uint32_t dlt_p2p_node::slot_scale() const { + const uint32_t iv = _delegate ? _delegate->block_interval() : CHAIN_BLOCK_INTERVAL; + return (iv > 0 && iv < CHAIN_BLOCK_INTERVAL) ? CHAIN_BLOCK_INTERVAL / iv : 1; +} void dlt_p2p_node::set_listen_endpoint(const fc::ip::endpoint& ep, bool wait) { _listen_endpoint = ep; _wait_if_busy = wait; @@ -1305,7 +1309,7 @@ void dlt_p2p_node::on_dlt_hello_reply(peer_id peer, const dlt_hello_reply_messag // fallen behind (missed broadcast blocks) — transition to SYNC // and request the missing range. uint32_t our_head = _delegate->get_head_block_num(); - if (state.peer_head_num > our_head + FORWARD_FALLBEHIND_THRESHOLD) { + if (state.peer_head_num > our_head + FORWARD_FALLBEHIND_THRESHOLD * slot_scale()) { ilog(DLT_LOG_ORANGE "Falling behind in FORWARD mode: peer ${ep} at #${pn}, our head #${hn} — transitioning to SYNC" DLT_LOG_RESET, ("ep", state.endpoint)("pn", state.peer_head_num)("hn", our_head)); transition_to_sync(); @@ -1368,7 +1372,7 @@ void dlt_p2p_node::request_blocks_from_peer(peer_id peer) { // on the majority fork, giving the chain a chance to switch forks. uint32_t start = our_head; uint32_t gap = (peer_latest > our_head) ? (peer_latest - our_head) : 0; - if (gap > FORWARD_FALLBEHIND_THRESHOLD) { + if (gap > FORWARD_FALLBEHIND_THRESHOLD * slot_scale()) { uint32_t our_lib = _delegate->get_lib_block_num(); if (our_lib > 0 && our_lib < our_head) { ilog(DLT_LOG_ORANGE "Large gap (${g} blocks behind peer ${ep}), starting sync from LIB #${lib} instead of head #${head}" DLT_LOG_RESET, @@ -3088,7 +3092,7 @@ void dlt_p2p_node::check_sync_catchup() { // the stagnation timer each time and delaying fork-switch recovery by // many minutes (p68 scenario). Stay in SYNC so the stagnation check // can fire at the right time and request the full alternative chain. - if (_highest_seen_block_num > our_head + FORWARD_FALLBEHIND_THRESHOLD) { + if (_highest_seen_block_num > our_head + FORWARD_FALLBEHIND_THRESHOLD * slot_scale()) { dlog("Staying in SYNC: highest seen block #${h} is ${d} blocks ahead of our head #${o} — unlinked fork_db blocks pending", ("h", _highest_seen_block_num)("d", _highest_seen_block_num - our_head)("o", our_head)); return; @@ -3157,7 +3161,7 @@ void dlt_p2p_node::check_forward_behind() { const auto& state = _peer_item.second; if (state.lifecycle_state != DLT_PEER_LIFECYCLE_ACTIVE) continue; if (state.peer_head_num == 0) continue; - if (state.peer_head_num > our_head + FORWARD_FALLBEHIND_THRESHOLD) { + if (state.peer_head_num > our_head + FORWARD_FALLBEHIND_THRESHOLD * slot_scale()) { ilog(DLT_LOG_ORANGE "Falling behind in FORWARD mode: our head #${h}, peer ${ep} at #${p} — transitioning to SYNC" DLT_LOG_RESET, ("h", our_head)("ep", state.endpoint)("p", state.peer_head_num)); transition_to_sync(); @@ -3474,7 +3478,7 @@ void dlt_p2p_node::track_fork_state(const signed_block& block) { } if (_fork_detected && - block.block_num() - _fork_detection_block_num >= FORK_RESOLUTION_BLOCK_THRESHOLD) { + block.block_num() - _fork_detection_block_num >= FORK_RESOLUTION_BLOCK_THRESHOLD * slot_scale()) { resolve_fork(); // _fork_detected is cleared inside resolve_fork() only when resolution completes } @@ -4159,11 +4163,12 @@ void dlt_p2p_node::check_wedge_watchdog() { // "Stuck behind" gate: far enough behind the CORROBORATED tip to be // suspicious. An under-corroborated tip is 0, collapsing `behind` to 0 and // tripping this branch — so an eclipsed / low-peer node never arms. - if (behind <= WEDGE_BEHIND_THRESHOLD) { + const uint32_t wedge_behind_threshold = WEDGE_BEHIND_THRESHOLD * slot_scale(); + if (behind <= wedge_behind_threshold) { if (_wedge_since != fc::time_point()) { ilog(DLT_LOG_GREEN "Wedge watchdog: caught up / under-corroborated (behind=${b} <= ${thr}, " "corroborating_peers=${p}), clearing wedge timer (our_head=#${h})" DLT_LOG_RESET, - ("b", behind)("thr", WEDGE_BEHIND_THRESHOLD)("p", established_peers)("h", our_head)); + ("b", behind)("thr", wedge_behind_threshold)("p", established_peers)("h", our_head)); } _wedge_since = fc::time_point(); return; @@ -4216,7 +4221,7 @@ void dlt_p2p_node::check_wedge_watchdog() { ("rc", rejects_climbed ? "climbing" : "flat")("r", _gap_rejected_total)); } - if (!is_wedged(behind, head_advanced, rejects_climbed, elapsed_sec)) return; + if (!is_wedged(behind, head_advanced, rejects_climbed, elapsed_sec, wedge_behind_threshold)) return; // ── Confirmed wedge ────────────────────────────────────────── elog(DLT_LOG_RED "Wedge watchdog: CONFIRMED network wedge — head frozen at #${h} for ${s}s while " diff --git a/libraries/network/include/graphene/network/dlt_p2p_node.hpp b/libraries/network/include/graphene/network/dlt_p2p_node.hpp index cb52f60e89..4ec7e5aedc 100644 --- a/libraries/network/include/graphene/network/dlt_p2p_node.hpp +++ b/libraries/network/include/graphene/network/dlt_p2p_node.hpp @@ -111,6 +111,9 @@ class dlt_p2p_delegate { virtual bool is_emergency_consensus_active() const = 0; virtual bool has_emergency_private_key() const = 0; virtual bool is_dlt_mode() const = 0; + /// Live slot length in seconds (3 before HF16, 1 after). Block-count + /// thresholds in the node are multiplied by CHAIN_BLOCK_INTERVAL / this. + virtual uint32_t block_interval() const { return CHAIN_BLOCK_INTERVAL; } // ── Block queries ──────────────────────────────────────────── virtual fc::optional read_block_by_num(uint32_t block_num) const = 0; @@ -386,7 +389,7 @@ class dlt_p2p_node { // Thresholds feeding the pure wedge predicate below. Public (with the // predicate) so the truth-table unit test can assert the boundaries // symbolically rather than hard-coding magic numbers. - static constexpr uint32_t WEDGE_BEHIND_THRESHOLD = 200; ///< Blocks behind network tip to be considered "behind" + static constexpr uint32_t WEDGE_BEHIND_THRESHOLD = 200; ///< Blocks (3 s slots) behind network tip to be considered "behind"; × slot_scale() at runtime static constexpr uint32_t WEDGE_CONFIRM_SEC = 900; ///< Wedge condition must hold this long (15 min) before acting // Pure predicate for the wedge state machine — no I/O, table-testable. @@ -404,8 +407,9 @@ class dlt_p2p_node { // (recency matters: one early rejection then silence must NOT confirm) // - genuine divergence trips all three, and only after WEDGE_CONFIRM_SEC. static bool is_wedged(uint32_t behind, bool head_advanced, - bool rejects_climbed, uint32_t elapsed_sec) { - return behind > WEDGE_BEHIND_THRESHOLD + bool rejects_climbed, uint32_t elapsed_sec, + uint32_t behind_threshold = WEDGE_BEHIND_THRESHOLD) { + return behind > behind_threshold && !head_advanced && rejects_climbed && elapsed_sec >= WEDGE_CONFIRM_SEC; @@ -441,6 +445,10 @@ class dlt_p2p_node { private: dlt_p2p_delegate* _delegate = nullptr; + /// CHAIN_BLOCK_INTERVAL / live slot (1 pre-HF16, 3 after). Block-count + /// thresholds below are written for 3 s slots; multiply by this to keep + /// their wall-clock meaning. + uint32_t slot_scale() const; std::string _user_agent; fc::ip::endpoint _listen_endpoint; bool _wait_if_busy = true; @@ -465,7 +473,7 @@ class dlt_p2p_node { static constexpr uint32_t SYNC_STAGNATION_MAX_RETRIES = 3; // ── FORWARD fallbehind ────────────────────────────────────── - static constexpr uint32_t FORWARD_FALLBEHIND_THRESHOLD = 2; ///< Blocks behind peer before FORWARD→SYNC + static constexpr uint32_t FORWARD_FALLBEHIND_THRESHOLD = 2; ///< Blocks (3 s slots) behind peer before FORWARD→SYNC; × slot_scale() at runtime static constexpr uint32_t FORWARD_BEHIND_GRACE_SEC = 15; ///< Grace period after SYNC→FORWARD before checking peers fc::time_point _forward_entered_time; ///< When we last entered FORWARD mode @@ -545,7 +553,7 @@ class dlt_p2p_node { static constexpr uint32_t BAN_DURATION_SEC = 3600; // ── Fork resolution ────────────────────────────────────────── - static constexpr uint32_t FORK_RESOLUTION_BLOCK_THRESHOLD = 42; + static constexpr uint32_t FORK_RESOLUTION_BLOCK_THRESHOLD = 42; ///< 3 s slots; × slot_scale() at runtime // ── Gap fill ────────────────────────────────────────────── static constexpr uint32_t GAP_FILL_MAX_BLOCKS = 100; ///< Max blocks per gap fill request diff --git a/plugins/p2p/p2p_plugin.cpp b/plugins/p2p/p2p_plugin.cpp index 063eebf91d..746cdb9ea5 100644 --- a/plugins/p2p/p2p_plugin.cpp +++ b/plugins/p2p/p2p_plugin.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -95,6 +96,23 @@ class dlt_delegate : public dlt_p2p_delegate { return chain.db()._dlt_mode; } + // The DB interval reads hardfork shared state, not an atomic scalar. + // Refresh under the standard guarded read; a transient lock/resize/recovery + // failure must not unwind P2P. Keep the last successfully observed interval + // and retry on the next call (including after snapshot restore or a reorg). + uint32_t block_interval() const override { + try { + const auto interval = chain.db().with_weak_read_lock([&]() { + return chain.db().block_interval(); + }); + if (interval > 0) + _cached_block_interval.store(interval, std::memory_order_relaxed); + } catch (...) { + // Thresholds remain usable while the chain is temporarily unavailable. + } + return _cached_block_interval.load(std::memory_order_relaxed); + } + // ── Block queries ──────────────────────────────────────────── fc::optional read_block_by_num(uint32_t block_num) const override { auto& db = chain.db(); @@ -481,6 +499,7 @@ class dlt_delegate : public dlt_p2p_delegate { chain.clear_syncing(); } + mutable std::atomic _cached_block_interval{CHAIN_BLOCK_INTERVAL}; chain::plugin& chain; fc::time_point _startup_time; ///< P22: startup timestamp for dead-fork grace period }; diff --git a/plugins/validator/validator.cpp b/plugins/validator/validator.cpp index 9727c85a6d..fbbeefd3e6 100644 --- a/plugins/validator/validator.cpp +++ b/plugins/validator/validator.cpp @@ -346,7 +346,7 @@ namespace graphene { // Fork collision resolution state uint32_t fork_collision_defer_count_ = 0; - uint32_t _fork_collision_timeout_blocks = 21; // one full validator round (21 blocks = 63s) + uint32_t _fork_collision_timeout_blocks = 21; // one full validator round (21 slots: 63 s before HF16, 21 s after) // Minority fork recovery state: tracks when we rolled back to // LIB and are waiting for P2P sync to catch up before diff --git a/tests/consensus_sim/scenarios/test_wedge_predicate.cpp b/tests/consensus_sim/scenarios/test_wedge_predicate.cpp index e952d58504..765f8d41b4 100644 --- a/tests/consensus_sim/scenarios/test_wedge_predicate.cpp +++ b/tests/consensus_sim/scenarios/test_wedge_predicate.cpp @@ -73,6 +73,10 @@ BOOST_AUTO_TEST_CASE(behind_boundary_is_strictly_greater) { BOOST_CHECK(!dlt_p2p_node::is_wedged(BEHIND, false, true, LONG_ENOUGH)); // One block past the threshold does. BOOST_CHECK(dlt_p2p_node::is_wedged(BEHIND + 1, false, true, LONG_ENOUGH)); + // HF16 (1 s slots): the node passes WEDGE_BEHIND_THRESHOLD * 3 so 200 blocks + // keeps meaning ~10 minutes; the boundary moves with the threshold. + BOOST_CHECK(!dlt_p2p_node::is_wedged(BEHIND * 3, false, true, LONG_ENOUGH, BEHIND * 3)); + BOOST_CHECK(dlt_p2p_node::is_wedged(BEHIND * 3 + 1, false, true, LONG_ENOUGH, BEHIND * 3)); } BOOST_AUTO_TEST_CASE(confirm_boundary_is_inclusive) { diff --git a/tests/p2p/test_slot_interval.py b/tests/p2p/test_slot_interval.py new file mode 100644 index 0000000000..90ee19df97 --- /dev/null +++ b/tests/p2p/test_slot_interval.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Compile the real delegate read and node scale/predicate against a guarded DB fixture. +No live DB access. Run inside Docker; CXX may select the compiler. +""" +from pathlib import Path +import os +import re +import subprocess +import tempfile +from test_corruption_recovery import method +ROOT = Path(__file__).resolve().parents[2] +plugin = (ROOT/'plugins/p2p/p2p_plugin.cpp').read_text() +node = (ROOT/'libraries/network/dlt_p2p_node.cpp').read_text() +header = (ROOT/'libraries/network/include/graphene/network/dlt_p2p_node.hpp').read_text() +read = method(plugin, 'uint32_t block_interval() const override') +scale = method(node, 'uint32_t dlt_p2p_node::slot_scale() const').replace('dlt_p2p_node::', '') +predicate = method(header, 'static bool is_wedged(') +cache = re.search(r'mutable std::atomic _cached_block_interval\{CHAIN_BLOCK_INTERVAL\};', plugin) +assert cache, 'delegate cache must be atomic' +cpp = r''' +#include +#include +#include +#include +constexpr uint32_t CHAIN_BLOCK_INTERVAL=3; +struct DB { + bool available=true, guarded=false; uint32_t interval=3; + uint32_t block_interval() const { assert(guarded); return interval; } + template auto with_weak_read_lock(F f) -> decltype(f()) { + if(!available) throw std::runtime_error("DB unavailable"); + guarded=true; auto value=f(); guarded=false; return value; + } +}; +struct Chain { DB value; DB& db() {return value;} }; +struct Base { virtual uint32_t block_interval() const=0; virtual ~Base()=default; }; +struct Delegate:Base { + Chain& chain; + explicit Delegate(Chain& c):chain(c){} +''' + cache.group() + '\n' + read + r''' +}; +struct Node { + Base* _delegate=nullptr; + static constexpr uint32_t WEDGE_BEHIND_THRESHOLD=200, WEDGE_CONFIRM_SEC=900; +''' + scale + '\n' + predicate + r''' +}; +int main() { + Chain chain; Delegate d(chain); Node node; assert(node.slot_scale()==1); + node._delegate=&d; + chain.value.available=false; assert(d.block_interval()==3); // lazy safe default + chain.value.available=true; assert(d.block_interval()==3); assert(node.slot_scale()==1); + chain.value.interval=1; assert(d.block_interval()==1); assert(node.slot_scale()==3); + chain.value.available=false; assert(d.block_interval()==1); assert(node.slot_scale()==3); + chain.value.available=true; chain.value.interval=3; assert(node.slot_scale()==1); // restored pre-HF state + chain.value.interval=0; assert(d.block_interval()==3); // invalid value cannot poison cache + chain.value.interval=1; + const auto s=node.slot_scale(); + assert(!(6>2*s)); assert(7>2*s); + assert(!(125>=42*s)); assert(126>=42*s); + assert(!Node::is_wedged(200*s,false,true,900,200*s)); + assert(Node::is_wedged(200*s+1,false,true,900,200*s)); + assert(!Node::is_wedged(200*s+1,false,true,899,200*s)); +} +''' +with tempfile.TemporaryDirectory(prefix='viz-slot-') as tmp: + source=Path(tmp)/'test.cpp'; binary=Path(tmp)/'test'; source.write_text(cpp) + subprocess.run([os.environ.get('CXX','c++'),'-std=c++14','-Wall','-Wextra','-Werror',str(source),'-o',str(binary)],check=True) + subprocess.run([str(binary)],check=True) +print('PASS: guarded delegate, 3->1->3, unavailable DB fallback, invalid interval, all scaled boundaries')