Add ASCII fork choice tree to terminal logs#143
Conversation
Updates the pinned leanSpec commit hash to pick up the latest spec changes.
leanSpec commit 0340cc1 changed XMSS Signature serialization from structured
JSON (path/rho/hashes sub-objects) to hex-encoded SSZ bytes ("0x..."). Replace
the old ProposerSignature struct and its SSZ reconstruction logic with a simple
hex deserializer (deser_xmss_hex), matching the existing deser_pubkey_hex pattern.
…lidation Port two validation rules from leanSpec 8b7636b: reject attestations where the head checkpoint is older than the target, and reject attestations where the head checkpoint slot doesn't match the actual block slot. Well-formed attestations already satisfy these, but without them we'd accept malformed ones.
At interval 3, the migration step (interval 4) hasn't run yet, so attestations that entered "known" directly (proposer's own attestation in block body, node's self-attestation) were invisible to the safe target calculation. Merge both pools to avoid undercounting support. No double-counting risk since extract_attestations_from_aggregated_payloads deduplicates by validator ID.
After aggregating committee signatures at interval 2, broadcast the resulting SignedAggregatedAttestation messages to the network. aggregate_committee_signatures and on_tick now return the new aggregates, and BlockChainServer::on_tick publishes them via P2PMessage::PublishAggregatedAttestation. The variant already existed but was never sent from the aggregation path.
… attestation subnets configurable. Previously hardcoded to 1, this allows future devnets to use multiple committees. The value is passed to the P2P layer for subnet subscription (validator_id % committee_count).
…ivision by zero in subnet calculation
… by CLI validation
…ng usage with local devnet and standalone nodes,
color coding, layout guide, and JSON API schema
Print a decorated fork choice tree at interval 4 (end-of-slot) so developers can see chain shape, forks, and weights directly in the terminal without opening a browser. The tree shows finalized/justified/head checkpoints, linear trunk until first fork, Unicode branch connectors, missing-slot indicators, head marker (*), leaf weights ([w:N]), and depth truncation at 20 nodes.
🤖 Kimi Code ReviewReview SummaryThis PR introduces a fork choice tree visualization feature. The code is generally well-structured and follows Rust best practices. Here are my findings: Issues Found
Security Considerations
Code Quality Suggestions
Minor Issues
The implementation is solid overall and follows good Rust practices with proper error handling in most places. The visualization will be very helpful for debugging fork choice issues. Automated review by Kimi (Moonshot AI) · custom prompt |
🤖 Codex Code ReviewSummary Findings
Consensus-layer review If you want next steps
Automated review by OpenAI Codex · custom prompt |
Greptile SummaryAdds ASCII fork choice tree visualization logged to terminal at end-of-slot (interval 4), enabling developers to monitor chain state without external tools. Key changes:
Implementation quality:
Confidence Score: 5/5
|
| Filename | Overview |
|---|---|
| crates/blockchain/src/fork_choice_tree.rs | New ASCII fork choice tree formatter with comprehensive tests, clean implementation using String::fmt::Write, proper depth truncation, and well-structured recursive rendering |
| crates/blockchain/src/lib.rs | Simple module declaration restricted to pub(crate) visibility as intended |
| crates/blockchain/src/store.rs | Adds log_tree parameter to control fork choice tree logging, only enabled at interval 4 to avoid excessive output, integrates cleanly with existing fork choice logic |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[on_tick - Interval 4] --> B{log_tree == true?}
B -->|Yes| C[accept_new_attestations]
C --> D[update_head]
D --> E[compute_lmd_ghost_head]
D --> F[compute_block_weights]
F --> G[format_fork_choice_tree]
G --> H[TreeRenderer.render_trunk]
H --> I{Fork point?}
I -->|Yes| J[TreeRenderer.render_branches]
I -->|No| K[render single chain]
J --> L[render_branch_line for each child]
L --> M{Sub-fork?}
M -->|Yes| J
M -->|No| N[Leaf node with weight]
G --> O[info! log to terminal]
B -->|No| P[Skip logging]
Last reviewed commit: e5892ae
🤖 Claude Code ReviewNow I have enough context for a thorough review. Let me compile it. PR #143 Review — ASCII Fork Choice TreeSummaryClean addition of a pure ASCII renderer with no new dependencies and a solid test suite. The logic is generally sound. A few correctness and performance issues worth addressing before merging. Bugs
This means that if the head block sits inside a linear segment whose tip is a deeper descendant — a realistic scenario when a new block extends a chain but doesn't yet have enough weight to become the new head — the
1 => {
if is_head { write!(output, " *").unwrap(); }
write!(output, "── ").unwrap();
...
}The same guard needs to be added in
blocks
.iter()
.filter(|(_, (_, parent))| parent.is_zero() || !blocks.contains_key(parent))
.min_by_key(|(_, (slot, _))| *slot)
.map(|(root, _)| *root)
.expect("blocks is non-empty")Two problems:
Both are low-probability in normal operation but worth guarding against. Return an Performance
let weights =
ethlambda_fork_choice::compute_block_weights(finalized.slot, &blocks, &attestations);This runs again immediately after. For a large live chain, you're walking all attestations twice per slot. Consider exposing the weights alongside the head from Minor Issues
Test GapsThe unit tests are well-structured and cover the documented cases. Two scenarios worth adding:
What's Good
Automated review by Claude (Anthropic) · custom prompt |
…add missing tests - Change module and function visibility from pub to pub(crate) since the formatter is only used within the blockchain crate. - Replace .cloned().unwrap_or_default() with slice pattern matching to avoid allocating a new Vec on every branch iteration. - Add tests for: single-block chain, depth truncation at MAX_DISPLAY_DEPTH, and nested forks (sub-fork within a branch).
|
@greptile-apps review this PR again |
compute_lmd_ghost_head now returns (H256, HashMap<H256, u64>) so callers can reuse the weights computed during head selection. This eliminates the duplicate compute_block_weights call in update_head when logging the fork choice tree.
Motivation
This PR adds an ASCII tree directly to terminal logs so developers can monitor fork choice state without leaving the terminal.
Description
Adds a new
fork_choice_treemodule that renders the fork choice tree as ASCII art and logs it once per slot at interval 4 (end-of-slot attestation acceptance).Output format
Conventions
root(slot)format using ShortRoot (8 hex chars)──connectors for linear chains├──/└──Unicode tree characters for forks*marker on the head block[w:N]weight annotation on leaf nodes[ ]for single missing slot,[N]for N consecutive missing slots...when chain exceeds 20 nodesChanges
crates/blockchain/src/fork_choice_tree.rsformat_fork_choice_tree()pure formatter +TreeRendererstruct for recursive renderingcrates/blockchain/src/lib.rspub(crate) mod fork_choice_treedeclarationcrates/blockchain/src/store.rslog_treeparameter toupdate_head/accept_new_attestations; log tree at interval 4 onlyLogging frequency
To avoid excessive output, the tree is only printed at interval 4 (end-of-slot), giving one tree per slot. Block imports (interval 0) and
on_blockcalls passlog_tree: false.No new dependencies
The formatter uses only
std::fmt::Writeand types already available in the blockchain crate (H256,ShortRoot,Checkpoint).How to Test
Unit tests
├──/└──connectors and branch count[ ]indicator[N]indicator(empty)output*appears on the correct node...at MAX_DISPLAY_DEPTH (20)