Skip to content

Release 4.2.0: HF16 BFT finality, 1s blocks, DAO cap and operator UI/MCP - #181

Open
On1x wants to merge 116 commits into
masterfrom
pm
Open

On1x wants to merge 116 commits into
masterfrom
pm

Conversation

@On1x

@On1x On1x commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Summary

Release 4.2.0 activates HF16: BFT block votes/QC/TC, 2-chain finality, 1-second slots, certificate-based fork priority, equivocation evidence, the dynamic DAO request ceiling, and the no-contest fee freeze. It also adds the optional node_admin operator UI/MCP and fixes production with a pending signing-key rotation.

Activation is scheduled for 2026-10-31 00:00:00 UTC (1793404800), subject to the production 17/21 hardfork-version vote quorum. These timestamps are not placeholders.

HF16 consensus and economics

  • Header extensions 3/4/5 carry quorum certificates, timeout certificates and double-vote evidence; P2P messages 5117/5118 carry block/timeout votes. The new equivocation virtual operation is id 106.
  • QC quorum is max(floor(2N/3)+1, floor(0.75N)), i.e. 15 of 21. This differs from the 17/21 hardfork activation threshold.
  • LIB uses the certified consecutive-round 2-chain rule. Certificate rounds take priority in fork choice; TC and late-QC paths allow recovery after connectivity disruptions.
  • Block slots change from 3 seconds to 1 second. Issuance intentionally remains 1.000 VIZ per produced block (owner decision Q3105=C): gross annual issuance at continuous production rises from approximately 10.5M to 31.5M VIZ. Missed slots do not mint rewards.
  • Existing block-denominated period settings and undo capacity are migrated to preserve their wall-time meaning. Stored/voted properties remain native block counts (owner Q3108=A); operators must update hard-coded re-votes by three, or use the live CHAIN_DEFAULT_CHAIN_PROPERTIES / current on-chain values. get_config now exposes consistent live hour/day/year block counts and defaults, and reserve-ratio adjustment retains its 60-second cadence. Vote/equivocation history preserves its one-hour duration (3600 rounds after HF16). Key-blanking thresholds and the minimum distribution epoch also preserve their wall-time duration. Emergency activation remains a distinct 1200-second setting; it is not a 5-minute recovery guarantee.
  • DAO request maximum becomes floor(current_supply.amount / 100) in raw VIZ units; pre-HF16 replay retains the 500,000 VIZ rule. Existing requests are not repriced.
  • No-contest fees freeze at announcement. The separate frozen flag distinguishes a legitimate frozen zero from legacy rows; existing pre-fork rows preserve their settlement behavior.

Operator UI, MCP and signing-key fix

The optional node_admin plugin provides 41 scoped admin.* methods, a built-in /ui/ page and Streamable HTTP MCP on one authenticated listener. Features include node/P2P diagnostics, snapshots, logs, webserver policy, two-phase config/plugin edits, supervised restart/state repair, and validator actions signed inside validator_guard. The token stays in sessionStorage; no external UI assets are loaded. The listener defaults to loopback and requires explicit opt-in for remote access.

The signing-key fix reads the committed producer key after resetting the pending transaction session under the write lock. The validator plugin refreshes its committed-key snapshot from applied block contents rather than the database's restored pending session.

Upgrade requirements

All nodes must upgrade before activation, including validators, seeds, public API nodes and exchange nodes. 4.1.x cannot deserialize the new header extensions after HF16.

Shared-memory layouts change for validator/dynamic-global and PM objects. Rebuild state by replaying or loading a compatible 4.2.0 snapshot; do not reuse an old mapped state file. Production-config deployments must use the production image, not a BUILD_TESTNET binary (different chain id/quorum).

Full upgrade and protocol documentation is available in English, Russian and Chinese at docs/consensus/hf16.md and its translations, with UI/MCP references in docs/plugins/node-admin.md.

Validation and review follow-up

The pre-review head passed production/testnet CI and Docker image builds. A local 21-validator stand also completed six unavailable validators, 20% packet loss with delay, a 241-second full outage and a 10/11 partition, with finality restored on all nodes, without emergency keys or process restarts. These are measured scenarios, not a universal recovery SLA.

Review fixes are now in this PR through 7359f33b: certificate-priority admission verifies producer/quorum signatures; native-period validation preserves pre-HF replay and checks the live post-HF duration; property updates validate a candidate before touching live multi_index objects; time windows/API defaults preserve the documented cadence. Operator fixes cover secret masking, recovery/read serialization, MCP schemas, config persistence, proxy attribution, rate limits, stats flushing, audit retention, bounded listener ingress and atomic config writes. Snapshot HTTP downloads now stream in 64 KiB chunks with backpressure and a 30-second stalled-write timeout, without a file-size ceiling. The temporary 8 MiB limit was removed because it rejected normal ~40 MB testnet snapshots.

Validation of the review changes:

  • Docker build passed for vizd and all four regression binaries.
  • Local HF16 boundary suite (6 cases) and all three no-contest fee states passed in the classic one-validator testnet configuration.
  • Admin utility, capped logger and PM property unit suites passed.
  • Live node_admin acceptance and additional smoke passed, including restore with concurrent polling, proxy/limit policy, MCP schemas/audit, pre-auth connection bounds, symlink config/restart precedence and streaming snapshot transfers.
  • EN/RU/zh documentation build passed; the audit found 99 user pages per language, no missing translations or broken file targets, and all 41 admin methods. This does not certify every anchor or every translated sentence.
  • A two-validator testnet CI matrix now requires a real scheduled quorum and tests TC sibling replacement. The N=2 matrix and production Docker image build passed on final head 57f99e7c. The full local consensus suite and all CI jobs passed on 57f99e7c. The subsequent snapshot-only follow-up 89d38e1b rebuilt vizd and passed the full live admin acceptance + smoke and EN/RU/zh docs build, including a 64 MiB + 123-byte download with SHA-256 verification, HEAD length, bounded RSS under two stalled clients, concurrent admin RPC, timeout and disconnect handling. Production CI on 89d38e1b exposed a Boost 1.91 API difference: the timer cancel(error_code&) overload is unavailable. Follow-up 7359f33b uses portable cancel() with exception handling, rebuilt vizd and passed the full live admin acceptance/smoke again, including the stalled-write timeout regression. The production Boost 1.91 Docker build and 21-validator testnet image build passed on 7359f33b (run 40085797170). All final-head CI checks have now passed: production/testnet21 Docker images, Boost 1.83 floor build, testnet N=1 full simulator, testnet N=2 HF16 suite, and the distro consensus/PM/unit CTest job (10/10 registered suites, zero failures). Verified against exact PR head 7359f33b, runs 40085797170 and 40085797073.

Per-thread review replies identify completed fixes and the deliberately retained owner decisions. The recommendation to split this release into separate PRs is not being applied. Merge to master remains with the owner.

On1x added 27 commits October 16, 2026 04:09
docker.md and validator-node.md already recommend it; getting-started
(en/ru/zh-CN) and README did not, so a node started from them stays
down after a host reboot.
pm_no_contest today opens a 12-hour dispute window and leaves the penalty to be
computed from the median chain_properties_pm of the settlement moment, so a
parameter vote inside the window silently repriced an announced void. The fork
records the fee on the market at announcement and charges exactly that.

Consensus:
* pm_market_object gains no_contest_fee + no_contest_fee_frozen. The flag is
  separate from the amount on purpose: a row announced before the fork has
  frozen == false and keeps the legacy median computation (no seed pass), while
  a legitimate frozen zero (penalty percent voted to 0) must not fall back to a
  later vote.
* pm_no_contest_evaluator freezes max(0, dispute_fee * pct / 10000) at
  announcement; settle_market(win < 0) charges the frozen value under HF16.
* pm_oracle_object gains no_contest_slashed_total, grown in the same modify()
  as total_insurance_slashed, for exact attribution without a virtual op.
* Registration in hardfork.d/16.hf + database_hardfork.cpp, CHAIN_VERSION 4.2.0
  in both configs, snapshot import guarded by contains() for the new fields.
  Activation timestamps are placeholders pending the owner's mainnet date.

API (no consensus):
* get_oracle / list_oracles: no_contest_pending_count / _stake / _fee_nominal /
  _fee_capped over by_oracle_finalized (open obligations only, per-block cache).
* market_card: settlement_charge — the price of a void before it is charged.
* get_pm_chain_properties: derived pm_no_contest_fee_per_market, additive.

Proof: tests/consensus_sim/scenarios/test_pm_no_contest_fee.cpp drives three
markets around the activation block (legacy, frozen 1u, frozen 0), moves the
median afterwards, and measures the insurance drop and the counter: 9u total
(8 + 1 + 0). pm_lifecycle_suite: 72 tests, no errors.

Docs: operator checklist in English and Russian, API reference updated.
…ly v1)

An administrative JSON-RPC endpoint inside the node process, for operator tooling
and AI agents. It opens its own HTTP listener rather than riding on webserver:
that one is published through nginx, so every request already arrives as
127.0.0.1 and a source-address check there protects nothing.

- Authentication: `label:scope[,scope]:sha256hex[:expires-unixtime]`, only the
  digest is stored, comparison is constant-time, and a token carrying no `read`
  scope gets -32002.
- Guards: loopback-only unless `admin-allow-remote`, no listener without tokens,
  POST only, 64 KB body cap, 20 req/s per token, log tails restricted to an
  explicit `admin-log-file` allow-list.
- Read-only methods: ping, node_info, diagnostics, plugins.list, config.get,
  logs.tail, audit.tail. Numbers are emitted as JSON numbers, not fc's default
  quoted form.
- Every outcome, including failed authentication and rate limiting, appends one
  JSON audit line with a label, never the token or its hash.
- p2p: add get_connections_count_sync() — the peer map belongs to the p2p thread,
  so diagnostics reads it through fc::thread::async().wait().
- Docs in English and Russian; live acceptance in
  plugins/node_admin/tests/acceptance.sh (35 checks against a throwaway node).

Mutating methods, plan/apply and restart are the next stage.
Add the Model Context Protocol (Streamable HTTP) to the same loopback
listener, with the same bearer tokens and audit trail (q#3052=B: no
sidecar). Methods: initialize, ping, tools/list, tools/call;
notifications are answered with 202 and an empty body. Tools map 1:1 to
the read-only admin.* methods and report execution errors via isError,
while protocol errors stay JSON-RPC errors. Sessionless, no SSE.

Extend acceptance with 16 MCP checks (51 total, all passing).
Expose the P2P picture the node already logs (head/LIB, DLT range, lag behind
the best peer, uptime, per-peer lifecycle and ban state) as admin.p2p.status.
Add the three operator actions the owner asked for: admin.p2p.add_node,
admin.p2p.add_trusted (never soft-banned, kept through known-peer GC) and
admin.p2p.clear_banned. Mutating methods need a token with the p2p scope on
top of read; all three are serialized onto the P2P thread.

Serve a minimal static page at GET /ui/ from the same listener: token in
sessionStorage, status cards, peers table, add/trusted/clear actions, five
second refresh. The page is unauthenticated by design and carries no data;
CSP restricts it to inline assets and same-origin fetch.

Expose the same four methods as MCP tools (11 total) and cover everything in
the acceptance suite (80 checks, including scope denial and the UI route).
Add plan/apply editing of config.ini behind the admin API:

- config.plan validates allow-listed keys (p2p, dlt, webserver, validator-guard),
  normalizes booleans and range-checks integers, and returns the exact diff
  without writing anything. plugins.plan validates the plugin list and refuses
  unknown names, duplicates and removal of chain/node_admin.
- Plans are single-use, bound to the token that created them, expire after
  admin-plan-ttl seconds, and are capped at 64 outstanding entries.
  plan.cancel discards one explicitly.
- apply recomputes the file hash and refuses when config.ini changed since the
  plan, then rewrites raw lines (comments, order, spacing and CRLF preserved)
  through a temp file, fsync and rename, after writing a .bak-<unixtime>
  backup. A config_write_mutex serializes concurrent applies.
- config.backups lists the backups under the plain read scope.
- admin.restart (process scope) asks appbase to quit after the reply flushes;
  the supervisor is what brings the node back, and the reply says so.

Acceptance covers scope and token binding, allow-list, expiry, stale hash,
cancel, replay, backups, plugin protection, a real restart and a
supervisor-style start that reads the edited config back. Unit tests cover the
line editors, normalization and plugin guards.
Add admin.validator.list / plan / apply (scope "chain") and the matching MCP
tools. Reads stay under "read"; plan and apply require a token with "chain"
AND a started validator_guard configured with active keys, so the scope alone
never authorises a signature.

validator_guard gains chain_actions_available(), validators_status() and
apply_validator_actions(). Actions are built and signed inside a weak read
lock, then accepted and broadcast after the lock is released (the same rule
disable_on_shutdown follows). Plans carry the observed signing key; apply
re-checks it and reports "chain state changed" instead of signing a stale
intent.

A successful disable writes an operator hold to
<data-dir>/validator-guard-holds.txt. The periodic auto-restore skips held
validators and startup reads the file back, so a redeploy cannot silently
re-enable block production. The hold follows the outcome, never the intent:
a failed disable leaves no latch, and enable/set_key clear it.

Docs (EN and RU) describe the methods, the plan/apply contract, the hold
semantics and the test coverage; acceptance.sh now covers the gate (guard
missing, guard loaded without an active-key validator) and the tool count.
The signing happy path was verified on a scratch chain with the validator
plugin and cli_wallet.
Covers admin.validator.list/plan/apply on a scratch testnet chain:
disable/enable/set_key, hold persistence across restart, stale plan
rejection, a two-action plan (one tx per action), and contract corners.

Staging notes in the script: register/vest/vote the producing validator
before HF9, run operator actions on a non-scheduled validator, and never
rotate the producing validator's own key (the producer reads the signing
key from session state, so the update is lost - tracked separately).

Docs (EN/RU) name the script and both load-bearing decisions.
A block header is validated against the validator signing key committed at the
previous block, but the producer read the key from the session-visible validator
object, where a pending validator_update is already applied. Rotating the
producing validator's own key made it sign the block with the new key:
validate_signee failed, the pending pool was cleared and the update was lost.

- database: check the signing key after _pending_tx_session.reset(), reading the
  committed object under the same write lock (a concurrent push_transaction after
  the lock is released would make the object session-visible again).
- validator plugin: keep a committed-key snapshot. Seed it at startup and refresh
  it from applied block contents - an applied_block callback runs after
  push_block() has re-applied the pending pool, so a DB read there sees
  uncommitted state. effective_signing_key() returns the committed key when the
  session-visible one differs and the snapshot is for the current head.
- validator-e2e: new section 10 rotates the producing validator's own key and
  rotates it back; the chain keeps producing and the key changes on chain
  (62 passed, 0 failed).
The paragraph still described the pre-fix behaviour (the producer read the
session-visible key, so rotating the producing validator's key lost the
update) and even pinned a source line number. That premise no longer holds:
the producer signs with the key committed at head, and section 10 covers
self-rotation. Reword to what cold is actually for and drop the line ref.
The page always called '/' for JSON-RPC and '/snapshot/…' for downloads, so it only
worked when the listener was reached at its root. Operators also publish it through
nginx under a path (e.g. /admin/ui/), where those absolute URLs leave the admin
listener entirely.

Derive the base from location.pathname (strip a trailing '/ui' or '/ui/'), exactly as
public API clients do for their own base. At the root the value is '/', so direct
access is unchanged. Documented in both locales.
Boost >= 1.87 dropped boost::asio::io_service, which the vendored
websocketpp 0.8.2 still spells ~100 times. webserver_plugin.cpp already
includes fc/compat/asio_compat.hpp for this; node_admin did not, so the
production image (Boost 1.91) failed to build the plugin while a local
build against an older Boost succeeded.
ctest found eight tests but the workflow built only the targets listed in
tests/pm and tests/consensus_sim, so tests/node_admin registered its
node_admin_util test and ctest reported it as "Not Run" (exit 8) while
every binary that was built passed. Take the list from every
tests/*/CMakeLists.txt under tests/.
…ve logs

The built-in /ui/ page could not tell a read-only token from an admin one:
Create snapshot, Restore latest and the peer actions were always shown, the
API refused them only after the click. It also knew nothing about emergency
consensus and could not read the node's own log.

Backend:
- admin.auth.me returns the token's label and scopes (never the token/hash).
- admin.snapshot.status takes offset/limit (default 20, max 200) and reports
  snapshot_total and has_more.
- admin.logs.list names the allow-listed appenders with size and existence.
- admin.validators.active returns the scheduled validator names, the schedule
  size and the emergency consensus state.
- admin.node_info gains emergency_consensus_active/start_block,
  emergency_key_configured and recovering.

UI:
- Status / Peers / Snapshots / Logs / Validators tabs.
- actions are hidden when the token lacks the scope they need.
- snapshots: newest 20 with Load more; the download button disables itself and
  shows a radial spinner, so repeated clicks cannot start parallel downloads.
- logs: tail an allow-listed file, paused by default, Play polls every 2s and
  appends at the bottom; auto-scroll yields when the operator scrolls up.
- status shows the emergency consensus state and whether an emergency key is
  configured; snapshot state says what idle/creating/restoring means.

Acceptance: auth.me, logs.list, validators.active, paging fields and the
emergency fields are now checked; tool count 37.
…the tabbed UI

Describe admin.auth.me, admin.logs.list, admin.validators.active and the new
offset/limit fields of admin.snapshot.status. Rewrite the built-in UI section
around the five tabs, scope-driven action hiding, Load more paging, the
disabled download button with its radial spinner, and the paused-by-default
live log tail. Both the English and the Russian document, same content.
…fset

A snapshot written between two pages shifted every offset below it, so the UI
repeated a row on Load more; the 24 h retention sweep shifts the other way and
made offsets skip a row. admin.snapshot.status now takes after=<last name the
client has> and continues after that exact row; a pruned anchor is reported as
after_found=false and offset applies. The UI anchors to the last shown row,
dedupes by name and restarts from page 1 when its anchor was pruned.
The stock file appender rotates hourly and keeps a day, so debug logging can
grow to gigabytes per hour. Add an opt-in capped_file appender that keeps one
file with only the last max_lines lines (default 10000, 0 = unlimited).

- trim walks the file backwards in 64 KiB chunks, so a startup on an oversized
  log costs a bounded tail read rather than a full scan; the rewrite goes to a
  temp file and is renamed into place
- compaction is amortised: it runs only after the file passes max_lines plus
  10% slack, and the appender counts lines as it writes, so no per-line stat
- nodes without a log.capped_file_appender.* section are unaffected; the
  appender is registered with its own type name and stays unknown to older
  binaries
- node_admin already reads any path, so admin.logs.list/tail pick the file up
  with no API change

Tests: tests/logging (8 cases) cover the tail contract and live compaction.
The file appender rotates hourly and keeps a day, which is not a size cap: at
debug level that is still gigabytes per hour. Say so, and point operators at
the optional capped_file appender for a hard line limit.
The file appender is configured to rotate hourly with one-day retention, but the
rotation chain runs on fc's fiber queue and vizd's main thread lives in
boost::asio after startup, so it never runs. Measured 2026-10-20: a testnet
node wrote one 37 MB file in nine hours at info, with no hourly files; the
mainnet node ran four days without a new one.

Correct the logging docs in en/ru/zh and the node-admin page: the file is
bounded by neither time nor size, and capped_file is the only real cap.
…he file

The line counter ran after a failed trim as well, i.e. exactly when the log is
largest (a full disk is a plausible reason the trim itself failed). Reading the
whole file into an ostringstream there would allocate a second copy of a log that
may be gigabytes — the appender would OOM while reporting that it cannot cap the
file. Count in 64 KiB chunks instead; a missing file still counts as zero lines.

Tests: three new cases (25k lines, missing file, a line spanning chunk borders).
Measured on real images: an older binary (4.1.2, no capped appender) starts fine with a
config that contains the new section, ignores it and keeps logging to stderr — so the
section can be deployed before the upgrade. A new node whose path cannot be opened also
starts, prints one 'cannot open log file' line to stderr, and leaves no file.
tests.yml now discovers every binary named by add_test, so it builds all
six pm_* targets (plus node_admin_util and capped_file_appender) with the
same configure flags and runs the whole ctest — a superset of the
`ctest -R '^pm_'` performed here. The only PRs it skips are docs-only
ones, where there is nothing to test.

So the `pm-tests` label no longer adds coverage; the gaps that remain are
`check-boost-range` (the only job building `vizd` itself on Boost 1.83)
and `testnet-config` (the only job running consensus_sim with testnet
constants, where the hardfork registry is validated). A skipped gate is
not a passing one: a PR with zero labels shows these as skipped next to
green checks.
@On1x On1x added check-boost-range Also run the distro Boost 1.83 range-floor build testnet-config Compile the testnet config and run consensus_sim (fork-gated PM regressions) labels Oct 20, 2026
Owner decision (Q#3081=A): the plugins panel shows the existing allow-list
grouped by plugin, plus log settings. Two gaps were in the way.

Sectioned keys. The config editor only touched top-level options, so
logger.* and log.* could not be edited at all. config_spec now names its
ini section, and plan/apply address the line through
set_config_value_in_section(). A missing section is an error rather than an
insert: inventing [logger.foo] would configure a logger the node never
declares and the next start would ignore it silently. CRLF and the spacing
around '=' are preserved exactly as in the top-level editor.

Logging keys. logger.default.level and logger.p2p.level accept
all/debug/info/warn/error/off; log.capped_file_appender.node.max_lines and
.flush bound the file that can fill the disk. Appender filenames stay
operator-only.

API for the UI. admin.config.get gains 'allow' -- key, group, section, type,
current value (null when the file relies on the code default), bounds and
allowed values -- so the panel renders without a second source of truth.
admin.plugins.list gains 'removable' (chain and node_admin render as
non-toggleable rather than merely off) and 'params' per plugin.

Verified by -fsyntax-only on both translation units; tests for the new
editor and normalizer are in node_admin_util_test.cpp.
// (database.cpp _generate_block injects hardfork_version_vote while current_hardfork_version <
// CHAIN_HARDFORK_VERSION); the fork then applies on the first block at or after this time.
//
// SET by the owner (2026-10-20): 2026-10-31 00:00:00 UTC is the mainnet date, and the validator

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — scope, description and activation date do not match the code.

The PR body says "Activation timestamps are placeholders in this branch; the mainnet date is not announced", while this file says "SET by the owner (2026-10-20): 2026-10-31 00:00:00 UTC is the mainnet date". One of them is wrong, and the code ships the 2026-10-31 value in both configs.

More important: this header (and 0-preamble.hf:55, config.hpp:5) describes HF16 as the no-contest fee freeze. What the branch actually activates at CHAIN_HARDFORK_16:

  • Jolteon-style block votes / quorum certificates / timeout certificates in header extensions 3, 4, 5 (base.hpp, block_vote.hpp), validator_equivocation_operation (id 106)
  • 2-chain finality replacing the 75 % confirmation LIB (highest_qc_on_main_branch)
  • block interval 3 s → 1 s (CHAIN_BLOCK_INTERVAL_HF16, database::block_interval()), round re-anchoring, ×3 scaling of block-denominated chain props, undo history ×3
  • fork choice by certificate round before the HF12 rule
  • committee request ceiling 500 000 VIZ → 1 % of current supply
  • new P2P messages 5117/5118 and hello capability bits

The PR title/body, this descriptor and the release notes need to say that. Two consequences the body does not mention: every 4.1.x node (seeds, API nodes, exchanges) stops deserializing headers at the first block carrying static_variant index 3, and the shared-memory layout changes include validator_object and dynamic_global_property_object, not only the pm objects, so every node replays or restores from a 4.2.0 snapshot. Seven days of lead time for a block-interval + finality change is very short for that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 57f99e7: the PR title/body, HF16 descriptor and preamble now describe BFT QC/TC, 2-chain finality, 1-second slots, emission, DAO cap and wire/layout changes. Activation is explicitly 2026-10-31 00:00 UTC subject to the version quorum; the stale placeholder statement is gone. Upgrade/replay requirements cover validators, seeds, API and exchange nodes.

#define CHAIN_CLEAR_CLOSED_COMMITTEE_REQUEST_DELAY fc::days(7)

#define CHAIN_BLOCK_INTERVAL 3
#define CHAIN_BLOCK_INTERVAL_HF16 1 // HF16 (#6737, Q#3103=A): slot after the fork block; database::block_interval()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — per-block emission is not scaled, so annual emission triples at HF16.

process_funds() (HF11 path, database.cpp:4674) mints CHAIN_DIGITAL_ASSET_ISSUED_PER_BLOCK = 1.000 VIZ on every block, unconditionally. With 1 s slots that is ~31.5 M VIZ/year instead of ~10.5 M. database_hardfork.cpp ×3-scales inflation_recalc_period, distribution_epoch_length, pm_batch_epoch_blocks, pm_reveal_window_blocks, but nothing touches emission, and docs/consensus/hf16.md does not mention it.

If 3× emission is intended, it must be stated in the hardfork doc and the validator announcement. If not, divide by CHAIN_BLOCK_INTERVAL / block_interval() under the HF16 gate (and decide what to do with the 0.333 remainder so supply stays exact).

Same family, lower stakes: CHAIN_MAX_VALIDATOR_MISSED_BLOCKS 200 ("~10 min") becomes ~3.3 min and CHAIN_EMERGENCY_MAX_VALIDATOR_MISSED_BLOCKS ~105 s; CHAIN_MIN_DISTRIBUTION_EPOCH_LENGTH ("1 hour") becomes 20 min for post-fork votes. Worth an explicit decision each.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (documentation); issuance is intentional: the owner selected 1.000 VIZ per produced block (Q3105=C), so gross annual issuance at continuous production increases from about 10.5M to 31.5M VIZ. It is now explicit in the PR/HF16 docs in EN/RU/zh. Done for the other live timing issues: missed-key thresholds and minimum distribution epoch preserve wall time after HF16; vote/equivocation retention preserves one hour.

Comment thread libraries/chain/database.cpp Outdated
new_head->id != new_block.id() &&
new_block.block_num() <= new_head->num && // equal, or below a late-QC head
new_block.block_num() > get_dynamic_global_properties().last_irreversible_block_num &&
block_cert_round(new_block) > block_cert_round(new_head->data)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fork choice acts on an unverified certificate.

block_cert_round(new_block) reads the QC/TC round straight out of the header extensions. At this point nothing about the block has been verified: validate_block() only checks the merkle root and block size, and the producer signature and block_qc_quorum are checked in apply_block → validate_block_qc. Same at line 2157 (cert_cmp).

So a peer without any validator key can send one block at head height with a fabricated QC round and force set_head + switch_forks: pop the current suffix, apply the forged block, fail in validate_block_header/validate_block_qc, recovery-pop, restore — all under the write lock, once per fresh block id. Pre-HF16 the attacker needed a longer chain; now one block is enough, and the comment at 2152 acknowledges the revert as the defence.

Suggest verifying at least the producer signature against the scheduled key and block_qc_quorum on the carried certificate before its round is allowed to influence fork choice (both are cheap relative to a switch), or gating this path to blocks that passed validate_block_header.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 57f99e7: certificate-based priority now requires a valid producer signature, scheduled quorum and verified QC/TC signatures before a same-height/lower-height replacement or certificate-priority rollback. A forged same-height certificate regression checks that no honest suffix is replayed. If cheap admission cannot establish a certificate under the current keys/schedule, its claimed round receives no priority; the historical longer-chain path and full branch validation remain available for legitimate competing state.

Comment thread libraries/chain/database.cpp Outdated
const account_name_type &validator, const signature_type &signature) {
// Same window as block votes: bounded memory for valid-but-absurd rounds.
static constexpr uint32_t MAX_VOTE_AHEAD_ROUNDS = 64;
static constexpr uint32_t MAX_VOTE_ROUND_BEHIND = 1200; // ~1 h at 3 s

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_VOTE_ROUND_BEHIND = 1200 // ~1 h at 3 s — rounds are 1 s after HF16, so this is 20 minutes. Same literal for the equivocation window at line 2969 and 7250. If an hour was the intent, derive it from block_interval(); otherwise fix the comments. Note that the equivocation window also bounds how long after the fact a proof can still be submitted on-chain.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 57f99e7: vote/timeout retention and the on-chain equivocation window now use 3600 / block_interval() rounds, preserving one hour after HF16. Widened round additions avoid overflow. Regression coverage checks a 2000-round-old vote is accepted and a vote outside the hour is rejected.

Comment thread libraries/chain/database.cpp Outdated
dpo.head_block_number > 0 ? dpo.head_block_number - 1 : 0);
// HF16 diagnostics (21-validator stand): a vote for head that is turned away
// is logged once per block, so a QC that never forms names its cause.
static uint32_t head_reject_logged = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function-static shared across all database instances. tests/consensus_sim runs several databases in one process, so the throttle counter is mixed between nodes. Member variable, please (same for skip_diag_head in the validator plugin if that is also static).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 57f99e7: head rejection throttling is a database-instance atomic member; validator TC skip diagnostics are plugin-instance members rather than function statics.

// on the HF16 QC stand, #6741). Every block will still be fully validated from genesis on
// apply, so accepting the peer costs nothing; a peer that cannot serve from #1 (a DLT node
// with a pruned range) still needs a snapshot and is not accepted here.
if (_delegate->get_head_block_num() == 0 && hello.dlt_earliest_block <= 1) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A node with head 0 accepts any peer that claims dlt_earliest_block <= 1. That is fine if --checkpoint still gates the early chain, but a fresh node bootstrapping from the shipped seed list otherwise takes its genesis-to-head history from whichever peer answers first. Worth a comment on what protects this path, or requiring the hello to carry a known block id at a checkpoint height.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (clarification) in 57f99e7: this hello grants transport eligibility only. Blocks are still checked against the locally configured genesis/chain id and checkpoints. A claimed genesis-serving range is not proof of canonical history; operators requiring a pinned bootstrap must configure known checkpoints or use a trusted snapshot.


void produce(simulated_node& n, const genesis_params& gp, fc::time_point_sec& when) {
vote_for_head(n, gp);
when += fc::seconds(CHAIN_BLOCK_INTERVAL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-HF16 the block interval is 1 s, but this helper steps CHAIN_BLOCK_INTERVAL (3 s), so after set_hardfork(16) every produced block skips two rounds. The single-validator fee test does not care, but any HF16 scenario built on this helper would be exercising the wrong cadence; use n.db().block_interval().

Bigger gap: there is no CI scenario for QC formation, LIB advance via highest_qc_on_main_branch, TC-based head drop, the late-QC fallback or validator_equivocation_operation. test_hf16_boundary.cpp only checks the slot and the ×3 scaling. The 21-validator stand runs quoted in the comments were manual. tests/consensus_sim already has a multi-node harness; at least one scenario of "N validators vote → QC → LIB moves → equivocation proof lands" should run on every PR.

@On1x On1x Oct 24, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in PR head 57f99e7: both the block producer helper and no-contest grace-block calculation use db.block_interval(). The new HF16 suite drives two independent databases through QC/LIB advancement, late-QC, TC inclusion, double-vote punishment and virtual operation 106, plus native-period and forged-certificate regressions. Local BUILD_TESTNET one-slot HF16 and all three fee states pass. The one-slot fixture immediately finalizes each head, so its TC case skips a stalled round without producing a conflicting irreversible block; a new N=2 testnet CI matrix requires two real scheduled validators and exercises TC replacement of an uncertified sibling. Final verification: both N=1 full simulator and N=2 HF16 CI passed on current PR head 7359f33. The distro CTest job also passed 10/10 registered suites with zero failures. The full local consensus suite passed previously, and the later snapshot-only changes passed live admin acceptance/smoke, including 64 MiB SHA-256, bounded memory, concurrent RPC and timeout checks. Production Boost 1.91 and floor Boost 1.83 builds are green.

}

// ── secret masking ──────────────────────────────────────────────────────────────────────────
inline bool sensitive_key(const std::string &key) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — admin.config.get leaks validator_guard's signing and active WIFs to any read token (MCP included).

Masking is by key name only. validator-guard-validator and its alias witness-guard-witness match none of the needles, yet their value is name signing_wif active_wif. A read-only monitoring token or an LLM agent calling tools/call admin.config.get receives the active key in plain text; the UI parameters panel shows it too.

Mask on the value as well (run mask_text over it) or add these keys to the list, and extend the sensitive_key_names unit test to cover them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 57f99e7: both validator-guard-validator and witness-guard-witness are sensitive keys; config values also pass through free-text WIF redaction. Adjacent WIFs are all masked, not just the first. Unit regressions cover the aliases and adjacent keys.


// ── methods ──────────────────────────────────────────────────────────────────────────────
fc::mutable_variant_object node_admin_plugin::impl::make_node_info() const {
const graphene::chain::database &db = chain.db();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chain reads on the admin thread with no lock, including during a restore.

make_node_info (here) and h_validators_active (~4612) read head_block_time(), get_dynamic_global_properties() etc. without with_weak_read_lock and without checking is_recovering(). The same file takes the lock at ~4343 with a comment on why, and h_snapshot_status (~3546) checks is_recovering().

The UI polls admin.node_info every 5 s. Click "Restore latest": restore_from_snapshot runs db.close(), which clears the index map, then rebuilds shared memory, while the poller indexes a cleared or half-built index — UB / segfault. Outside restores it is a plain data race with the P2P write thread.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 57f99e7: node info, active validators and object inspection use a recovery/read lifetime gate plus chainbase read locking; validator reads and snapshot status/head capture are also gated. Restore/auto-recovery own the gate through close/import, so a flag check cannot race the actual read.

#ifndef _WIN32
// rotate at a fixed size instead of growing without bound
struct stat st;
if (::stat(audit_path.c_str(), &st) == 0 && st.st_size > static_cast<off_t>(kAuditRotateBytes)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audit is not append-only in practice: one rotated generation, rename overwrites the old .1, and every unauthenticated request writes a line before rate_ok runs (auth comes first). ~200k junk POSTs (~32 MB) push both generations out; seconds on loopback, remote with admin-allow-remote. Either rate-limit before auditing unauthenticated requests, or keep N generations.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 57f99e7: a global ingress budget runs before request parsing/auth-failure auditing, and excess ingress is not appended to the audit. Rotation retains eight generations rather than overwriting a single backup. MCP audit entries include the actual tool name.

? options.at("webserver-admin-state").as<string>()
: (boost::filesystem::path(data_dir) / "webserver-admin.json").string();

{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webserver-admin.json is loaded after the options and replaces stats_enabled, limits_enabled and the per-minute/hour/day limits, and save_admin_state_locked writes all of them on any change (even a single whitelist add). So after the operator has ever touched the whitelist, admin.config.plan/apply {webserver-limit-m: …} + restart reports success and silently does nothing. Either persist only what the admin API actually changed, or have the state file win only for keys it explicitly carries.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 57f99e7: persisted runtime policy is tied to the startup stats/limits config_policy fingerprint. A changed config.ini policy supersedes stale persisted limits after restart; IP lists survive. Legacy files without a fingerprint restore lists only. The live acceptance regression checks that config.apply followed by restart really uses the new limit.


// Reading a file backwards costs one pass over the tail only; refuse to walk forever
// through a degenerate file that has no line breaks at all (a single 10 GB "line").
const uint64_t kMaxScanBytes = 64ull * 1024 * 1024;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kMaxScanBytes = 64 MB caps the backward scan, but find_tail_offset needs to walk max_lines lines. At ~200 B/line anything above ~330k lines (allow-list permits 10 000 000) makes every compaction fail: warn() fires once, lines resets to 0, the file grows without bound — the disk-fill the appender exists to prevent. Either cap max_lines consistently with the scan budget or scan in chunks. Also: multi-line messages (exception details) count as one line, so the cap overshoots by that factor. No unit tests for this class.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 57f99e7: backward scanning remains chunked but no longer stops after 64 MB; multiline records count their physical lines. Added regressions retain a tail larger than the old scan budget and enforce compaction for multiline messages. The existing appender test suite remains in CTest.

// Tools call the admin methods 1:1, so every tool invocation lands in the audit log with
// its own method name (`tools/call` is the JSON-RPC method; the tool name is in params).
fc::variant node_admin_plugin::impl::mcp_tools() const {
auto schema = [](fc::variants props, fc::variants required) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inputSchema.properties is emitted as an array of {type, description}; JSON Schema requires an object keyed by argument name, and required: ["name"] / additionalProperties: false refer to names that do not exist here. Strict MCP clients reject tools/list or every argument; lenient ones cannot discover that admin.logs.tail takes name/bytes/lines. Every tool with arguments is unusable over MCP as-is.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 57f99e7: every inputSchema.properties is now an object keyed by the actual argument names, including empty schemas. The live acceptance check verifies all tool schemas use object properties and every required key exists.

@chiliec chiliec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: request changes

Read the full diff (152 files) against origin/master (abee329) at 73d7d4d. Inline comments carry the detail; this is the ranking.

Blockers

  1. The PR describes a different change than it ships. Title/body/16.hf/preamble say "node_admin + signing-key fix + no-contest fee freeze". The code activates BFT block votes/QC/TC, 2-chain finality, 1 s blocks, fork choice by certificate round, a new virtual op, a committee ceiling change and new P2P messages at CHAIN_HARDFORK_16_TIME = 2026-10-31 00:00 UTC, while the body calls the timestamps placeholders. (16.hf comment)
  2. Emission ×3. process_funds mints a fixed 1.000 VIZ per block; nothing scales it for 1 s slots. Either intended-and-undocumented or a monetary-policy bug. (config.hpp comment)
  3. admin.config.get leaks validator_guard WIFs to any read/MCP token: masking is by key name and validator-guard-validator is not in the list. (node_admin_util.hpp comment)
  4. Unlocked chain reads on the admin thread (make_node_info, h_validators_active) while the UI's own "Restore" closes the database. (node_admin_plugin.cpp comment)

Consensus / network — should fix before mainnet

  • Fork choice (push_block sibling set_head, cert_cmp) trusts an unverified QC/TC round; one forged block from an unauthenticated peer forces a fork switch + revert under the write lock. (database.cpp:2122)
  • 1200-round windows were sized for 3 s and are now 20 min. (database.cpp:7191)
  • No CI coverage of QC/TC/LIB/late-QC/equivocation paths; the HF16 boundary test checks slot + ×3 only, and test_pm_no_contest_fee steps 3 s after forcing HF16. The 21-validator stand runs were manual.
  • Function-static diag counters shared between database instances.
  • Fresh node (head 0) accepts any peer claiming to serve genesis.

Operator surface — should fix before release

  • Audit log is erasable by unauthenticated traffic (single rotation, auth before rate limit).
  • webserver-admin.json silently overrides the config.ini keys that admin.config.apply edits.
  • capped_file_appender stops enforcing its cap above ~330k lines (64 MB scan budget), while the allow-list permits 10M.
  • MCP inputSchema.properties is an array; tools with arguments are unusable over MCP.
  • Webserver stats flush rewrites every tracked IP once a minute under stats_mutex, stalling admit(); both call sites pass force=true so the delta path is dead (webserver_plugin.cpp:621, :1268).
  • Rate limits key on the TCP peer: behind nginx every client is 127.0.0.1 and the whole public API 429s past 1000 req/min; ip_stats.clear() at 200k entries lets a limited client reset itself by spraying sources (webserver_plugin.cpp:268, :493).
  • Admin listener has no read timeout (timeout_open_handshake = 0) and no connection cap; fd exhaustion before auth with admin-allow-remote (node_admin_plugin.cpp:2063).
  • Minor: admin.plugins.status misreports multi-name plugin = lines (:3154); config temp file is created with umask perms before chmod and rename replaces a symlinked config.ini (:266–:291); snapshot download reads the whole file into memory on the listener thread (:2628); tool calls are audited as tools/call, not by tool name.

Looks right

  • Committed-signing-key fix: key check after _pending_tx_session.reset() under the strong write lock, snapshot refreshed from block contents, not DB. Correct reasoning.
  • validate() bounds loosened to 3 years while evaluators keep the 1-year bound pre-HF16: old and new nodes agree.
  • pm_batch_epoch_blocks / pm_reveal_window_blocks ×3 scaling and the block_interval() conversion in pm_commit_bet are consistent (no double scaling).
  • Vote safety rule (may_vote/may_timeout), persisted round, pacemaker backoff, re-gossip backoff: coherent Jolteon implementation with honest comments.
  • node_admin scope model, token hashing, path-traversal checks, plan/apply SHA guard, hold-file semantics.

Suggested split

Land node_admin + webserver + signing-key fix as 4.1.3 now (after blockers 3–4). Ship the BFT/1 s/emission/committee changes as their own PR with their own descriptor, docs, a CI multi-validator scenario, and an announced date with real lead time.

@On1x On1x changed the title Release 4.2.0: node_admin plugin, validator signing-key fix, HF16 no-contest fee freeze Release 4.2.0: HF16 BFT finality, 1s blocks, DAO cap and operator UI/MCP Oct 24, 2026
// own props each schedule round, so scaling only median_props would be
// undone within one round: scale every stored vote, then the median.
const auto scale = [](chain_properties& c) {
const auto x3 = [](uint32_t v) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One-shot ×3 of stored votes will erode. versioned_chain_properties_update_evaluator replaces the whole props struct with what the validator sends. Validator configs and the struct defaults (CHAIN_CONSENSUS_INFLATION_RECALC_PERIOD 806400, CHAIN_DEFAULT_DISTRIBUTION_EPOCH_LENGTH 28800, PM 20 / 200) are still 3 s-denominated. Each routine re-vote after the fork drops that validator's values to 1/3 wall time; after 11 of 21 the median follows. The evaluator cannot tell old-unit from new-unit values, so it cannot rescale or reject.

Convert at the use site instead, as the PR already does for COMMITTEE_REQUEST_PROCESSING (× (CHAIN_BLOCK_INTERVAL / block_interval())): database.cpp:4663, database.cpp:4808, pm_process_markets.cpp:1907, pm_evaluator.cpp:556-557. Stored unit stays stable, defaults and CHAIN_MIN_DISTRIBUTION_EPOCH_LENGTH keep their meaning, and this mutation, the validate() loosening to CHAIN_BLOCKS_PER_YEAR * 3 and the evaluator's check_year_bound become unnecessary.

Still unscaled:

  • CHAIN_MAX_VALIDATOR_MISSED_BLOCKS 200: 10 min → 3.3 min; CHAIN_EMERGENCY_MAX_VALIDATOR_MISSED_BLOCKS: 5 min → 105 s; CHAIN_EMERGENCY_EXIT_NORMAL_BLOCKS 21: 63 s → 21 s.
  • CHAIN_MIN_DISTRIBUTION_EPOCH_LENGTH "1 hour" → 20 min; its comment justifies the floor by _apply_block load.
  • Reserve-ratio step on head_block_number % 20 (database.cpp:6544): 60 s → 20 s.
  • get_config reports live CHAIN_BLOCK_INTERVAL but CHAIN_BLOCKS_PER_DAY etc. from the 3 s macros.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed the additional live timing/API issues in 57f99e7: reserve-ratio adjustment remains every 60 seconds; key-blanking thresholds and the minimum distribution epoch preserve wall time; get_config returns consistent live blocks/hour/day/year, undo capacity, thresholds and CHAIN_DEFAULT_CHAIN_PROPERTIES. Native retention/reveal validation uses the live cadence after HF16, retains the exact legacy rule before it, and rejects sum overflow after HF16.

The owner explicitly selected native block counts with a one-time votes+median migration (Q3108=A), so I retained that storage/voting contract rather than changing it to hidden legacy units. Re-voting stale script/struct defaults must be updated by the operator; this is explicit in EN/RU/zh docs and the PR body, and clients can use current on-chain values or live defaults.

CHAIN_EMERGENCY_EXIT_NORMAL_BLOCKS has no reader: actual emergency exit is by active-validator schedule quorum, not a 21-block timer. Marked the legacy constant accordingly instead of changing an inactive timer.

else ++it;
}
}
if (ip_stats.size() >= 200000 && !ip_stats.count(ip)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At 200k tracked IPs every unknown non-whitelisted source gets 429 until GC frees entries, and GC only evicts rows idle > 86400 s. A spray of 200k distinct sources (one IPv6 /64 is enough) fills the map in seconds and then blocks all new legitimate clients for up to 24 h. The memory bound became an availability DoS. Evict oldest last_seen (or shorten idle to the longest limit window, 86400 s only when per_day is set) instead of rejecting.

matrix:
validators: [1, 2]
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'testnet-config')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quorum path (CHAIN_TESTNET_VALIDATORS=2, real QC/TC/equivocation with two signers) runs only when the PR carries the testnet-config label. tests.yml covers N=1, where every head commits immediately and the TC branch is bypassed (scheduled_count > 1 is false). Without the label the HF16 BFT path has no CI coverage.

@chiliec chiliec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 7359f33. All 14 prior threads verified in code:

  • Fork choice: verified_fork_cert_round() gates certificate priority (producer sig, schedule membership, quorum size, per-vote signatures, QC/TC round bounds); forged_certificate_cannot_enter_fork_choice covers it.
  • Vote/equivocation windows 3600 / block_interval(), uint64_t widening on round arithmetic.
  • Missed-block thresholds, reserve-ratio cadence, HF16 min epoch, retention check against block_interval(); candidate props validated detached before modify.
  • get_config(live_block_interval) with scaled defaults, under weak read lock.
  • Emission ×3 and stored-props ×3 documented in 16.hf and docs/consensus/hf16.md as owner decisions.
  • node_admin: recovery try-lock + weak read lock on all chain reads, mask_text on config values, adjacent-WIF masking, validator_guard keys in the sensitive list, ingress budget before auth, 8-generation audit rotation, tool name in audit, MCP properties as objects, streamed snapshot download with 30 s stall timeout, mkstemp config write.
  • Webserver: policy fingerprint on state load, trusted-proxy XFF walk, lock-free stats I/O.
  • Appender: unbounded chunked scan, physical line count.
  • CI: N=1 and N=2 matrix, qc_tc_late_qc_and_equivocation exercises QC→LIB, late-QC, TC, op 106 on two databases. All checks green.

New, blocking: webserver fail-closed at 200k ip_stats entries lets a 200k-source spray lock out all new clients for up to 24 h (inline). Non-blocking: N=2 BFT coverage runs only with the testnet-config label (inline).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

check-boost-range Also run the distro Boost 1.83 range-floor build testnet-config Compile the testnet config and run consensus_sim (fork-gated PM regressions)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants