Repository navigation
Conversation
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.
… built-in rotation
…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.
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.
The verdict decides whether an operator is invited to wipe node state, so the judge now lives in objects_check.hpp with no fc or chainbase dependency, and a Boost.Test target drives every branch: healthy, crash lag (warn, not corrupted), truncation with and without a block log, exhausted memory alone (no repair offered) and beside another critical (replay), stale markers, schema mismatch, core index counts above and at genesis, the 5% free-space boundary, a wedged read lock (unreadable, no repair), and warn-only recovery/block-log cases. The plugin keeps the collector and maps the judgement onto the JSON response.
…eptance The acceptance script now drives the two features added this morning: a started plugin with its own view (p2p detail), an unlinked type answering state: null with a reason, a started plugin without a status API, and the missing-name refusal; then the chainbase view (index count matches the array, largest first, schema matches the binary, a clean verdict on a fresh node) and the repair refusals that must not write a marker (read scope, missing confirm, unknown mode, replay with an empty block log). The repair block only exercises paths that return before the marker is written, so the throwaway node survives the whole suite.
std::string::compare(pos, len, literal) compares a substring of length len against the whole literal, so compare(0, 2, "p2p-") never matches. Use the literal's own length, as the other branches already do. Caught by node_admin_util/config_groups_map_to_plugins in CI.
A node started with --replay-from-snapshot rebuilds state from the snapshot before it ever reaches the force_replay path, and comes back from the snapshot on force_resync rather than from genesis. The repair reply kept promising the generic block-log wording. Capture the startup options and say what will happen; expose snapshot_replay so a client can tell the cases apart. Docs updated in both locales.
Move repair_effect into the fc-free judge header so the snapshot-mode wording is covered like the verdict it accompanies: four shapes (plain replay, plain resync, snapshot-replay, snapshot-backed resync).
The suite proved the gate (read token, confirm word, empty block log) but never performed a repair, so a handler that refused everything would have passed it. Play the supervisor on the throwaway node: request a resync, wait for the clean exit, restart, and check that the startup log shows the marker was honoured and that the marker itself is gone.
config.ini writes '\''plugin = chain p2p'\'': appbase splits the value on whitespace and loads every name. The list kept the whole line as one name, so on a real config p2p had no lifecycle state, no status_method and no grouped parameters, and plugins.plan rejected 'remove p2p' as not configured. Split the value in the list, the diagnostics counter and the editor; removing one name from a multi-name line now rewrites the line instead of dropping its neighbours. The acceptance fixture had one name per line, which is why this never showed up in CI: it now mirrors the live shape, looks rows up by name instead of position, and checks that a multi-name line yields separate rows.
appbase reads config.ini through boost's config parser, which treats '#' as a comment to the end of the line. The admin view kept the comment inside the value, so `plugin = chain # why` listed "#" and "why" as plugin names, and a rewrite could drop the operator's note. Cut the value at '#', keep the tail in cfg_line::comment and re-append it when a line is rewritten.
Startup precedence is resync > replay: the chain plugin drops a force_replay marker when force_resync is present. The repair handler wrote its own marker and left a stale opposite one alone, so a confirmed "replay (keeps blocks)" under a leftover force_resync (the p2p wedge watchdog writes those) would resync and drop the block log — the response promising the opposite of what runs. Clear the opposite marker before writing ours, refuse with -32000 if it cannot be removed, and name it in removed_marker. Acceptance gains a second repair cycle that plants a stale force_replay, checks it is reported and gone, and that the next startup honours only the requested marker.
The chain plugin opens its database with <data-dir>/state as the data dir, so block_log and schema_version live next to shared_memory.bin. The objects collector read them from the application data dir: on every healthy node it reported "no schema, no block log", the judge turned that into verdict "corrupted", and the panel offered a resync. node_info read shared_memory.bin from the same wrong place, so its size was silently missing. Resolve schema_version and block_log under <data-dir>/state, and shared memory through chain.get_state_dir() (shared-file-dir). A block log carrying only chainbase's one-byte placeholder now counts as empty: replay_available is false and a replay repair is refused instead of reindexing zero blocks. objects.list always emits lock_error (null when the lock answered) so a client can tell "no error" from "field missing". Acceptance gains a block-log presence check; the unit suite drives the placeholder boundary.
The suite was written but never executed: it needs a built vizd, and host builds are forbidden here. Run against the CI image it reported 30 failures, and 27 of them were one environment defect — a Docker container without --user leaves the data dir root-owned, so the suite's own fixtures (snapshots, config.ini edits, repair markers) failed with permission denied and the cascades looked like product failures. Stop that class at the source: refuse to run when the node's state dir is not writable by this user, and document the --user invocation in the header. Also bring the suite in line with the code it tests: 41 MCP tools (the four objects and plugins.status tools were added), plus existence checks for them, and an explicit check that the block log is found in <data-dir>/state.
edit_plugin_lines() receives lines from split_lines(), which removes the '\n' and keeps only a trailing '\r'; join_lines() puts the newline back. The CRLF case passed "...\r\n" in, so the rewrite dropped the newline the fixture had smuggled in and the unit test failed on CI. Feed the fixtures the shape production actually passes, and assert the round trip through join_lines. The inline-comment case had the same malformed fixture and only passed because the comment carried the tail.
Validators asked for documentation links next to plugins. Plugins with a page of their own link to it, the rest to their section of plugins/overview, unknown ones to the overview itself. node_admin's own page ships with this branch and goes live with the master merge.
Status output used to land in a box at the very bottom of the Plugins tab, far from the button that produced it, and the parameters of all plugins were one flat list. Each row now has Status and Parameters buttons (disabled when the plugin has neither); below the table a tab per plugin with something to show holds its state, documentation link, refreshable status and its own parameters with Save. The buttons select the tab and scroll to the section, the selected row is highlighted, the choice survives a reload, and switching plugins asks before discarding unsaved parameter edits.
The per-plugin panel moved the answering method to a side note and dropped the 'state: ... (source)' line when a detail was available, so a copied status no longer said what was running or which call answered. Restore both at the top of the box. Caught by the live testnet UI acceptance harness.
The owner set the mainnet date. Both configs now carry 1793404800 instead of the unannounced placeholders (2026-11-15 testnet, 2026-12-01 production); the fee-freeze doc names the date in both locales.
transaction_id_type and block_id_type were declared as fc::ripemd160, a name inherited from Graphene, but neither is a ripemd160 digest: the transaction id is the first 20 bytes of sha256 over the unsigned transaction, the block id the first 20 bytes of sha224 over the header with the block number in bytes 0-3. Declare both through the alias id160_type (same type, same bytes, no consensus or wire change) and say so in types.hpp. checksum_type stays as is: the merkle root really is ripemd160 over the sha256 tree root. New page docs/api/ids-and-hashes.md (en, ru, zh-CN) with the exact recipes for clients and why the hash is not changed by a hardfork.
After a fork switch the post-validation signature for the dropped block has already left the node, and nothing stopped the validator from signing the replacement block at the same height. With enough validators doing that, both blocks of one height can reach the 2/3 threshold and different nodes advance LIB onto different blocks. Today forks are rare; with shorter slots they would not be. The validator plugin now records, per validator, the block id it signed at each height above LIB, writes the record before the signature is broadcast, persists it in <data-dir>/post-validation-signed.txt so a restart does not forget it, and refuses a different id at a height already signed (logged once per height). Re-signing the same id, which the 250 ms tick does by design, stays allowed. Signer-side rule only: no consensus or wire change, ships with 4.2.0 (HF16). Documented in consensus/block-processing (en, ru, zh-CN).
The DLT network redesign reduced broadcast_block_post_validation to a signature-less fork_status and dropped every apply_block_post_validation call, so validator signatures never left the node and LIB fell back to the producer rule (mainnet head-LIB = 11 blocks). - dlt_block_vote_message (5117): block_num, block_id, round, validator, signature over block_vote_digest (domain tag + chain_id + num + round + id). The validator plugin now signs this digest (round 0). - hello.capabilities (optional trailing field, ignored by v4.1.0): votes go only to peers advertising DLT_CAP_BLOCK_VOTE, so old peers never score an unknown packet against us. - database::push_block_vote: scheduled validator, signing-key recovery, height/round bounds, one vote per (validator, height, round); a second id is logged as equivocation and not counted. A quorum (>2/3, not below the irreversibility threshold) forms a node-local QC and logs the time from first vote to quorum. Consensus behaviour is unchanged: nothing reads the QC yet. LIB from QC, parent_qc in the header, skip votes and rounds follow under HF16.
update_last_irreversible_block takes the highest QC height that certifies the block on our main branch (capped at HEAD-1, skipped in emergency mode) when it is above the producer-rule LIB. A QC for a block we do not have, or for a sibling, is ignored. Node-local like the pre-DLT post-validation LIB it replaces; no evaluator reads LIB, so chain state stays deterministic.
New block header extension block_qc_certificate (variant index 3, appended): block_num, block_id, round and a flat_map of validator signatures over block_vote_digest. Rule (validate_block_qc, run in _apply_block before current_validator is overwritten): after HF16, a block whose parent was produced by a different validator must carry a QC of that parent - quorum of scheduled validators, each signature recovering to the voter's signing key. Exempt: a validator's own series, emergency mode, and the first block after an emergency (committee blocks never get a QC). The extension is rejected before HF16. Producer side: the validator plugin skips the slot while the head has no QC instead of forking; _generate_block attaches the certificate, re-verified against current state so it never carries a signature the consensus check would refuse. Vote verification is shared (verify_block_vote) between gossip intake and header validation. Known gap, next step: votes are round 0 only, so a split vote at one height can stall the handoff; rounds with growing timeouts and the lock rule follow.
A certified block is final only when its child at the next height is certified too, both on our main branch; LIB = that parent. A single QC is safe only while voting has one round: with rounds, a validator that voted for X need not see X's QC and may vote for Y later, so a second QC at the same height becomes possible. The child's QC (whose header carries X's QC) closes that window. Parent <= head-1 by construction, so the HEAD-1 cap still holds.
Round = slot. The parent is committed only when it and its child are certified on our branch AND the child sits in the very next slot. A child after skipped slots was built on a timeout certificate; a parent certified before that timeout is not yet safe to commit.
… step 3a) Votes now carry the block's slot as their round (timestamp / interval) instead of 0, end to end: validator signs block_vote_digest with it, p2p broadcast passes it, push_block_vote accepts a window around the head slot, and a parent QC must carry the parent's slot. The validator plugin replaces the one-signature-per-height guard with the Jolteon rule: vote for a block of round r only when r is above the highest round already voted. Candidates are voted in ascending round order; the round is persisted to <data-dir>/block-vote-round.txt BEFORE the vote leaves, and a vote whose round cannot be persisted is not sent. Unlike the height guard, a validator may vote for a different block at the same height in a later round, so a split vote can resolve instead of stalling the handoff. Equivocation tracking is keyed by (validator, round). Not yet complete: the lock half of the rule (vote only for a block whose QC is from the previous round, or that carries a TC) needs QC in every block and timeout certificates - the next steps. HF16 is not active on any network.
Drop the own-series exemption from block_needs_parent_qc. The Jolteon commit rule (two certified blocks in consecutive rounds) needs every block certified; with the exemption a series could build past a parent that never gathered a quorum, and LIB would stall on it. Emergency mode and the first block after it stay exempt.
…tep 4a) When a pushed block competes with head at the same or lower height, the branch whose tip carries the certificate of the later round wins; equal rounds fall through to the HF12 vote-weighted rule. The round is only claimed at this point: switch_forks applies the block and validate_block_qc rejects a forged certificate, which reverts the switch. Skipped in emergency mode. Groundwork for timeout certificates: a late block of a timed-out round becomes head but never gets a QC, and the block built on the TC must be able to replace it at the same height.
… parent QC a27dff3 made the parent QC mandatory for every block; the single-node simulator has no p2p, so pm_no_contest_fee_three_states (forces HF16) stalled on 'No QC for head'. Tally the producer's vote directly.
The scenario schedules two validators (initiator + genesis), so the QC quorum is two and the producer's vote alone never certified head #23. Move the vote out of the generic harness into the scenario, which knows both keys.
With a one-validator schedule (testnet, consensus_sim) the classic LIB equals head, so push_block_vote dropped the vote for head as 'already final' and HF16 could never certify head: the next producer stalled on 'No QC for head'. Settle only below head.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch adds an operator-facing admin surface to the node (
node_adminplugin), fixes aconsensus-adjacent block production bug, and adds HF16 (freeze the no-contest fee at
announcement). The admin surface is optional and inert unless configured.
node_admin plugin
A separate HTTP listener (loopback by default, bearer tokens, append-only audit) exposing 31
admin.*JSON-RPC methods plus:initialize,tools/list,tools/call), one tool per admin method, for AI agents./ui/— single static page, no external assets; the token stays insessionStorage. Works behind a reverse-proxy prefix (/admin/ui/) after this branch.add_node,add_trusted,clear_banned; actions are serialized onto the P2P thread.and blacklist, hot-reloadable and persisted next to the data dir.
plan/applywith a SHA-256 guard andatomic replace, plus
admin.restartfor a supervised restart.validator_guard(no extra keys stored), with a persisted hold file so a disabled operatorvalidator is not silently re-enabled by a restart.
Scopes are per token:
read(every method),p2p,snapshot,webserver,config,plugins,chain,process. A token that has only a mutating scope can call nothing withoutread; a missing scope is a separate error from a failed action. A non-loopback endpoint isrefused unless
admin-allow-remote = true.Bug fixed: a block producer signed with its own pending key
validate_block_header()checks the block signature against the validator's signing key frompre-block (committed) state, but
maybe_validate_block()read the session-visiblevalidator object and
_generate_block()asserted against that same object. A pendingvalidator_updateis already applied in the pending session, so rotating the producingvalidator's own key made the node sign block N with the new key while the header was validated
against the old one:
validate_signeefailed, the pending pool was cleared and the update waslost.
Fix:
database.cpp::_generate_blockchecks the key after_pending_tx_session.reset()andcaptures the committed key under the same write lock (otherwise a concurrent
push_transaction()can re-apply the pending change before the read).validator.cppkeeps a committed-key snapshot (_committed_signing_keys), seeded at startupand refreshed from the contents of applied blocks —
applied_blockfires afterpush_block()has restored the pending transactions, so the database there is not acommitted-state source.
Regression: new section 10 in
validator-e2e.shrotates the producing validator's key andback;
restart/disable/enable paths keep their coverage.HF16 — no-contest fee frozen at announcement (CHAIN_VERSION 4.2.0)
pm_no_contestopened a 12-hour dispute window but computed the penalty from the medianchain_properties_pmat settlement, so a parameter vote inside the window repriced anannounced void retroactively. HF16 records
no_contest_feeon the market at announcement(with a separate
no_contest_fee_frozenflag: pre-fork rows keep the legacy median path, alegitimate frozen zero does not fall back), charges exactly that at settlement, and adds a
monotone
no_contest_slashed_totalto the oracle in the samemodify()as the existingcounter. Snapshot import is guarded with
contains()for the new fields.Activation timestamps are placeholders in this branch; the mainnet date is not announced.
Testing
plugins/node_admin/tests/acceptance.sh— one-shot node, full HTTP/MCP/UI/auth surface.plugins/node_admin/tests/validator-e2e.sh— validator guard operator paths on a privatesingle-slot chain, including the self-rotation regression (62 checks, live run green).
tests/node_admin/— Boost.Test unit tests for the pure helpers (13 cases).tests/consensus_sim/scenarios/test_pm_no_contest_fee.cpp— three markets around theactivation block (legacy / frozen nonzero / frozen zero), median moved afterwards.
image from this PR. The plugin includes
fc/compat/asio_compat.hppbefore its websocketppheaders: Boost >= 1.87 dropped
io_serviceand the vendored websocketpp still spells it.Upgrade notes
node_adminis inert unlessadmin-endpointand at least oneadmin-tokenare configured;the plugin refuses to start with an endpoint and no token.
admin.config.applyare read at startup and needadmin.restart.pm_market_object/pm_oracle_object, so the shared-memory layoutchanges: redeploy testnet from a snapshot (the node already runs with
--replay-from-snapshot --snapshot-auto-latest).