Skip to content

[tontester] fix consensus explorer overflow for skipped slots, add leader stats - #2482

Open
yungwine wants to merge 7 commits into
ton-blockchain:testnetfrom
yungwine:explorer-fix
Open

[tontester] fix consensus explorer overflow for skipped slots, add leader stats#2482
yungwine wants to merge 7 commits into
ton-blockchain:testnetfrom
yungwine:explorer-fix

Conversation

@yungwine

Copy link
Copy Markdown
Contributor

No description provided.

@yungwine yungwine changed the title [tontester] fix consensus explorer overflow for skipped slots [tontester] fix consensus explorer overflow for skipped slots, add leader stats Jul 29, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 136999e3e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +283 to +286
for slot, collator in pairs:
if collator != first_collator:
leader_window = slot
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Derive the window width independently of the first observed slot

When --logs contains a truncated session or --stats-dir has retained only files beginning mid-group, the first collator transition occurs at an absolute slot such as 104 even when the leader window is 4. Assigning that slot number as the window makes the verification fail, so analyze_group() silently omits an otherwise valid group and the group API reports it as not found.

Useful? React with 👍 / 👎.

Comment on lines +291 to +295
collators_seen: set[int] = set()
for _, collator in pairs:
collators_seen.add(collator)

num_validators = len(collators_seen)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the configured validator count for incomplete rotations

For a current or partially retained group that has not yet exposed every collator, len(collators_seen) is only the number of validators observed so far, not the group's validator count. For example, the first 26 slots of a 100-validator group with a four-slot window are accepted as a seven-validator group, causing the API and aggregate output to omit the other validators and publish an incorrect total.

Useful? React with 👍 / 👎.

Comment on lines +288 to +289
if leader_window is None:
return None, None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Support groups whose collator never changes

A valid single-validator consensus group has the same collator for every slot, so this transition-based inference always leaves leader_window unset. Such groups are therefore discarded by analyze_group() even when they contain complete slot and finalization data, which breaks leader statistics for common single-node test configurations.

Useful? React with 👍 / 👎.

Comment on lines +100 to +104
# Walk parent chains from each finalized slot to reconstruct the
# finalized history. Visited slots are finalized (non-empty), slots
# skipped over between parent links are empty, everything else is
# unknown.
finalized_history = _walk_finalized_chains(directly_finalized, group_slots)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Follow the candidate named by each finalization certificate

When a collator equivocates or multiple candidates for one slot are present in the collected validator logs, SlotData retains whichever candidate was parsed last while directly_finalized retains only the slot number. Walking that unrelated candidate's parent chain can therefore mark canonical ancestors as skipped and noncanonical ancestors as finalized, corrupting the per-leader results precisely for the fork scenarios a consensus analyzer needs to diagnose.

Useful? React with 👍 / 👎.

Comment on lines +623 to +624
print(f"Leader stats server running at http://{host}:{port}{web_root}")
app.run(host=host, port=port, debug=False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Serialize parsing when serving raw logs

In --logs web-server mode, Flask serves requests concurrently by default, but all endpoints share one mutable ParserSessionStats instance whose cache and parsing dictionaries have no lock. If two initial requests or two requests after a log update overlap, one parse can append into the other's state and clear it while the other is still running, leaving duplicated or partial events cached and returning incorrect statistics; disable threaded serving or synchronize parser access.

Useful? React with 👍 / 👎.

if (showIdx) html += '<td>' + v.validator_idx + '</td>';
html += '<td>' + adnl + '</td>';
html += '<td>' + pkh + '</td>';
html += '<td>' + (v.name || '') + '</td>';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Escape validator names before assigning innerHTML

When --validator-names-json contains a validator name with HTML markup, the value is concatenated directly into a table that is later assigned through innerHTML. A legitimate name containing angle brackets corrupts the rendered table, and a payload such as an image with an event handler executes script in every viewer's browser when the server is shared; render this value as text or HTML-escape it first.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0bc4031ad5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +737 to +738
if text:
_print_text(analyzer, time_from, time_until)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wait for initial indexing before printing text stats

When --stats-dir --text is used with a new or stale index database, entering the FileIndex context only starts the indexing thread (file_index.py:111-114), while its initial scan runs asynchronously (file_index.py:404-411). This call can therefore query the database before files are indexed, print No groups found. or partial results, and immediately exit; wait for initial indexing to complete before producing the one-shot text report.

Useful? React with 👍 / 👎.

Comment on lines +250 to +253
if sd is not None and (sd.block_id_ext == "empty" or sd.is_empty):
history[current] = SlotStatus.EMPTY
else:
history[current] = SlotStatus.FINALIZED

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep finalized slots with missing candidate data unknown

When a finalization quorum is reconstructed from vote records but the corresponding candidate_received record is absent from the retained logs, _parse_vote_event still creates a SlotData whose block_id_ext remains None. This branch classifies that slot as FINALIZED (documented here as a finalized non-empty block), even though the finalized candidate may have been empty, so partial logs silently inflate finalized and undercount empty; preserve an unknown block type until candidate data is available.

Useful? React with 👍 / 👎.

Comment on lines +471 to +473
if (!timeFrom && !timeUntil && !groupFilter) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Let the default Load action fetch statistics

On the initial web page all three inputs are blank, the group field is explicitly described as optional, and the page tells the user to click Load, but this guard makes that button return without issuing a request or changing the page. Since /api/stats already supports an unfiltered request, users cannot load all available groups through the advertised default interaction and receive no explanation that a filter is required.

Useful? React with 👍 / 👎.

Comment on lines +97 to +101
# Gather slots with finalization certificates.
directly_finalized: set[int] = set()
for e in group_events:
if e.label == "finalize_reached":
directly_finalized.add(e.slot)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Account for skip certificates before a later finalization

When the latest observed slots have reached a skip quorum but no later candidate has finalized yet, the parser emits definitive skip_reached events for those slots, but this analyzer collects only finalize_reached. Because no later finalized parent chain exists to mark the gap, the trailing skipped slots are reported as unknown, understating each affected leader's skipped count for current or truncated sessions; seed the history from skip_reached certificates as well.

Useful? React with 👍 / 👎.

Comment on lines +611 to +614
def api_group(valgroup_name: str) -> Response | tuple[Response, int]:
stats = analyzer.analyze_group(valgroup_name)
if stats is None:
return jsonify({"error": "group not found"}), 404

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return 404 for malformed group names

In --stats-dir mode, a group filter that is not present in the index is passed to CachedGroupParser._resolve_name, which falls back to base64.b64decode; common typos such as foo raise binascii.Error. The exception escapes this handler and produces a 500 response instead of reaching the intended group not found 404 branch, so validate or catch unresolved group names before analysis.

Useful? React with 👍 / 👎.

yungwine and others added 2 commits July 29, 2026 21:03
`with sqlite3.connect(...) as conn:` manages the transaction, not the
connection, so every query left its db and -wal fds open until the cyclic GC
collected the connection. Connection/cursor cycles keep refcounting from
doing it, and gen-2 collections are rare in a process holding a large heap of
cached parse results, so the fds accumulated.

The mainnet leader_stats service hit its 1024 fd limit this way: of 1023 open
fds, 507 were stats_mainnet_new.db, 506 its -wal and 1 its -shm. Requests
then failed with "unable to open database file" and the server could no
longer accept connections. Repeating one query 600 times leaks 440 fds before
this change and none after.

Add a _connection() context manager that commits like `with conn:` did and
closes in a finally, and route all five call sites through it.

Also stop piping the sudo helper's stderr: nothing ever read it, so a helper
writing more than the pipe buffer would block forever, holding its fds and
the child process.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6623d2510f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# One pass over the union of their files: a file holds many groups, so
# parsing group by group re-reads and re-deserializes the same bytes.
data = self._parse_uncached([hashes[name] for name in missing], f"{len(missing)} groups")
split = split_by_group(data, missing)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Split results using the resolved canonical group name

When a caller supplies the valid base64 hash of a named group, _resolve_name() accepts it, but parsed GroupInfo, slots, and events use the canonical workchain,shard.seqno name. Splitting by the caller-provided base64 string therefore produces an empty ConsensusData, which is then cached under the real group hash; the request reports the group as missing, and subsequent canonical requests also receive the poisoned empty cache until invalidation. Resolve the canonical name before splitting or split by group hash.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants