Skip to content

refactor(ergo-node): split snapshot_emit.rs, snapshot.rs, scala_compat.rs, messaging.rs - #206

Merged
arkadianet merged 9 commits into
mainfrom
refactor/ergo-node-split-2-snapshot-messaging
Jul 17, 2026
Merged

arkadianet merged 9 commits into
mainfrom
refactor/ergo-node-split-2-snapshot-messaging

Conversation

@arkadianet

@arkadianet arkadianet commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

Second of two PRs splitting the largest/most tangled files in ergo-node (split from #204 to stay under coderabbitai's per-PR file-count limit — see #205 for the first half: wallet_bridge/boot). Same methodology used for the ergo-compiler splits (#200, #202): incremental extraction with test verification after each step, red-team review of every commit. Independent of #205 -- zero file overlap between the two PRs, so either can merge first.

  • node/snapshot_emit.rs + snapshot.rs: split into node/snapshot_emit/{mod,recent_blocks,bootstrap_panel,mempool_projection,events_projection}.rs (mod.rs keeps publish_snapshot as orchestrator) and snapshot/{mod,build,publisher}.rs (the NodeSnapshot/SnapshotParts DTOs stay whole in mod.rs — splitting a single wide DTO struct across files hurts more than it helps).
  • api_bridge/scala_compat.rs: light touch only — the trait impl (NodeChainQuery for ScalaCompatBridge) is one large block of small independent read-only methods, not worth forcing apart. Only the ~300-line fee-per-byte pool-ranking cluster moved to scala_compat/pool_fee_stats.rs.
  • node/messaging.rs: extracted the three sizable per-batch match arms (CODE_INV, CODE_MODIFIER ~190 lines, CODE_PEERS) into named helpers (handle_inv, handle_modifier_batch, handle_peers_response) following the same &mut NodeState -> Vec<Action> convention as handle_message/run_wallet_writer. Split into node/messaging/{dispatch,manifest,utxo_chunk,popow}.rs.

Every commit was independently verified (cargo test -p ergo-node including all integration tests, cargo clippy -p ergo-node --all-targets --all-features -- -D warnings, cargo fmt --all -- --check) and red-team reviewed by an independent Opus pass diffing against the pre-refactor original — no behavior-changing findings survived across any of the 3 splits. The whole-workspace gate (cargo clippy --workspace --all-targets --all-features -- -D warnings and cargo test --workspace) is also green.

Purely structural -- no functional changes intended.

Test plan

  • cargo test -p ergo-node (500 lib tests + full integration suite, all green) after each split
  • cargo clippy -p ergo-node --all-targets --all-features -- -D warnings clean
  • cargo fmt --all -- --check clean
  • cargo clippy --workspace --all-targets --all-features -- -D warnings clean (whole-workspace gate)
  • cargo test --workspace clean (whole-workspace gate, every crate)
  • Independent Opus red-team review per commit, diffing every moved function against the pre-refactor original

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi

Summary by CodeRabbit

  • New Features
    • Added operator API snapshots covering node health, sync progress, peers, mempool activity, recent blocks, events, and bootstrap status.
    • Added robust inbound peer-message dispatch for manifests, NiPoPoW proofs, UTXO snapshot chunks, peer discovery, and related synchronization flows.
    • Improved pool fee statistics with fee ranking, wait-time estimation, and histogram binning.
  • Bug Fixes
    • Hardened handling of malformed, duplicate, stale, or unauthorized inbound data across manifest, proof, and UTXO chunk processing.
    • Improved snapshot publication correctness with safer difficulty handling and more accurate stall/recovery tracking.

arkadianet and others added 3 commits July 17, 2026 07:48
…odules

node/snapshot_emit.rs -> node/snapshot_emit/{mod,recent_blocks,bootstrap_panel,
mempool_projection,events_projection}.rs: mod.rs keeps publish_snapshot as the
per-tick orchestrator; each field group it collects moves to its own submodule
(tip-cached recent-blocks tail + first-deliverer merge, Mode 2 bootstrap-panel
projection, mempool-transaction DTO list, operator event-feed projection).

snapshot.rs -> snapshot/{mod,build,publisher}.rs: mod.rs keeps the NodeSnapshot
and SnapshotParts DTOs plus their small support types (not split further --
splitting a single wide DTO struct across files hurts more than it helps);
build.rs assembles a NodeSnapshot from a SnapshotParts; publisher.rs owns
SnapshotPublisher's per-tick publish + stall-clock bookkeeping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi
…ol_fee_stats.rs

Light-touch split per plan: the trait impl block (NodeChainQuery for
ScalaCompatBridge) stays whole -- it's one large block of small,
independent read-only methods, not worth forcing apart. Only the
free-function fee-per-byte pool ranking cluster (PoolFeeEntry,
rank_pool_by_fee_per_byte, bin_for_wait_ms, estimate_wait_ms_from_rank,
their consts, and their tests) moves out to its own submodule; the
three call sites in pool_fee_histogram/pool_recommended_fee/
pool_expected_wait_time_ms are updated to the pool_fee_stats:: path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi
…submodules

Extract the three sizable per-batch match arms out of handle_message into
named helpers -- handle_inv (CODE_INV), handle_modifier_batch (CODE_MODIFIER,
~190 lines), handle_peers_response (CODE_PEERS) -- all following the same
&mut NodeState -> Vec<Action> convention as handle_message itself and the
dispatch pattern already used by run_wallet_writer. handle_message plus the
three new helpers live in messaging/dispatch.rs; the three Mode-2/NiPoPoW
consume-side handlers (handle_inbound_manifest, handle_inbound_utxo_chunk,
handle_inbound_popow_proof) each get their own submodule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@arkadianet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2ff84b5c-ed7c-408d-9e53-da6a32ffcad4

📥 Commits

Reviewing files that changed from the base of the PR and between ebfce45 and bfa3e66.

📒 Files selected for processing (1)
  • ergo-node/src/node/boot/mod.rs
📝 Walkthrough

Walkthrough

This PR extracts pool fee statistics, adds opcode-based inbound peer-message handlers, and rebuilds operator snapshot publication around modular DTO projections, snapshot assembly, and atomic publishing.

Changes

Pool fee statistics

Layer / File(s) Summary
Extract pool fee statistics
ergo-node/src/api_bridge/scala_compat/*
Pool fee ranking, wait estimation, histogram binning, overflow handling, and associated tests move into pool_fee_stats.rs; Scala-compatible endpoints call the extracted helpers.

Inbound peer messaging

Layer / File(s) Summary
Opcode dispatch and batch handling
ergo-node/src/node/messaging/{mod.rs,dispatch.rs}
Inbound frames are throttled, deserialized, routed by opcode, and processed through inventory, modifier, peer, section, and proof paths.
Manifest verification
ergo-node/src/node/messaging/manifest.rs
Incoming manifests are matched to snapshot-bootstrap ownership, checked against canonical headers and state roots, then accepted or rejected with voter eviction.
NiPoPoW and UTXO chunk handlers
ergo-node/src/node/messaging/{popow.rs,utxo_chunk.rs}
NiPoPoW proofs are decoded and validated with penalties for invalid results, while authenticated UTXO chunks update active assembly and silently drop benign races.

Operator snapshot publication

Layer / File(s) Summary
Snapshot contracts and initial state
ergo-node/src/snapshot/mod.rs
Defines NodeSnapshot, SnapshotParts, peer and delivery projections, the recent-block cache, snapshot handle, empty snapshot construction, and Unix-time calculation.
Recent block projection and caching
ergo-node/src/node/snapshot_emit/recent_blocks.rs
Builds a validated newest-first recent-block tail from the committed chain tip, caches contiguous results, and overlays first-deliverer data.
Bootstrap, event, and mempool projections
ergo-node/src/node/snapshot_emit/{bootstrap_panel.rs,events_projection.rs,mempool_projection.rs}
Projects bootstrap state, event-feed entries, and mempool transactions into API DTOs.
Snapshot assembly and publication
ergo-node/src/node/snapshot_emit/mod.rs, ergo-node/src/snapshot/{build.rs,publisher.rs}
Collects per-tick state, builds snapshot DTOs with sync and health classification, preserves difficulty values, tracks stall clocks, and atomically publishes snapshots.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • arkadianet/ergo#7: Uses snapshot-provided mining and maximum peer-height data in the Scala-compatible API.
  • arkadianet/ergo#152: Projects the event feed used by the operator events API.
  • arkadianet/ergo#178: Uses the pool fee ranking and wait-time logic extracted in this PR.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main refactor: splitting several large ergo-node modules into smaller submodules.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/ergo-node-split-2-snapshot-messaging

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@arkadianet
arkadianet marked this pull request as ready for review July 16, 2026 22:52
@arkadianet

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor
Action performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@arkadianet

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ergo-node/src/api_bridge/scala_compat/pool_fee_stats.rs`:
- Around line 57-87: The rank_pool_by_fee_per_byte function must use the active
network’s fee proposition bytes instead of MAINNET_FEE_PROPOSITION_BYTES. Thread
or obtain the existing network configuration used by the bridge, select its
mainnet/testnet fee proposition bytes, and use that value when filtering
transaction outputs while preserving the current ranking behavior.

In `@ergo-node/src/node/messaging/dispatch.rs`:
- Around line 390-394: Update the NiPoPoW proof-serving flow around
get_cached_popow_proof_bytes to handle as_utxo() returning None without
panicking: silently decline the request instead of calling expect. Preserve the
existing proof retrieval behavior for UTXO-backed state.
- Around line 552-563: Update the transaction branch in the dispatch flow to
derive the transaction ID from bytes and compare it with mod_id before calling
state.coordinator.on_transaction_received. Only invoke on_transaction_received
and admit_transaction when the IDs match; reject or ignore mismatches without
marking the claimed request as delivered.

In `@ergo-node/src/node/messaging/popow.rs`:
- Around line 46-66: Move the ERGO_CAPTURE_NIPOPOW_PROOF write logic out of the
pre-parsing section and into the verified BetterChain/NoBetterChain handling
paths in the surrounding message-processing function. Capture only after the
proof has passed parsing and genesis/chain validation, preserving the existing
one-shot path check and logging behavior so malformed or wrong-genesis proofs
are never written.

In `@ergo-node/src/node/snapshot_emit/bootstrap_panel.rs`:
- Around line 121-126: Update the in_catchup branch that builds the snapshot
metadata DTO to read the originally selected snapshot height and manifest ID
preserved in node state during installation, rather than using the advancing
best_full value and None. Ensure the install flow stores those selected values
and the post-install catch-up path reuses them unchanged.

In `@ergo-node/src/node/snapshot_emit/mod.rs`:
- Around line 78-117: Update the snapshot metadata assembly around
get_header_meta and genesis_block_id so empty-chain state is handled explicitly,
while live-chain lookup failures are treated as publication failures. Do not
replace missing or errored records with zero IDs, timestamps, scores, or genesis
IDs when live tip heights exist; log the storage fault and retain the previous
snapshot or skip publication. Preserve the existing read_tip_n_bits behavior and
valid metadata for successful lookups.

In `@ergo-node/src/snapshot/publisher.rs`:
- Around line 64-82: Update the difficulty carry-forward logic in the snapshot
publishing method to compare each current tip ID with its corresponding prior
snapshot tip ID before reusing prior_header_n_bits or prior_full_block_n_bits.
If the ID changed, return without publishing until the new difficulty is
readable; preserve carry-forward only when the corresponding tip ID is
unchanged. Update the related test to use distinct prior and current tip IDs and
cover this behavior.
- Around line 83-107: Update the progress tracking around last_header_height and
last_full_block_height so backward chain-height movement resets the
corresponding stall baselines before recovery advances are evaluated. Ensure
rollback from a previously observed height allows subsequent block application
through that height to refresh last_block_progress_at and prevents an actively
recovering node from being reported as stalled, while preserving normal
forward-progress handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 97148a6b-f7e8-4fd2-bcb1-f58d1b9414b2

📥 Commits

Reviewing files that changed from the base of the PR and between 42a9fd2 and 0b88263.

📒 Files selected for processing (18)
  • ergo-node/src/api_bridge/scala_compat/mod.rs
  • ergo-node/src/api_bridge/scala_compat/pool_fee_stats.rs
  • ergo-node/src/node/messaging.rs
  • ergo-node/src/node/messaging/dispatch.rs
  • ergo-node/src/node/messaging/manifest.rs
  • ergo-node/src/node/messaging/mod.rs
  • ergo-node/src/node/messaging/popow.rs
  • ergo-node/src/node/messaging/utxo_chunk.rs
  • ergo-node/src/node/snapshot_emit.rs
  • ergo-node/src/node/snapshot_emit/bootstrap_panel.rs
  • ergo-node/src/node/snapshot_emit/events_projection.rs
  • ergo-node/src/node/snapshot_emit/mempool_projection.rs
  • ergo-node/src/node/snapshot_emit/mod.rs
  • ergo-node/src/node/snapshot_emit/recent_blocks.rs
  • ergo-node/src/snapshot.rs
  • ergo-node/src/snapshot/build.rs
  • ergo-node/src/snapshot/mod.rs
  • ergo-node/src/snapshot/publisher.rs
💤 Files with no reviewable changes (3)
  • ergo-node/src/node/messaging.rs
  • ergo-node/src/node/snapshot_emit.rs
  • ergo-node/src/snapshot.rs

Comment on lines +57 to +87
pub(super) fn rank_pool_by_fee_per_byte(
pool: &[(ergo_primitives::digest::Digest32, std::sync::Arc<[u8]>)],
) -> Vec<PoolFeeEntry> {
let mut entries: Vec<PoolFeeEntry> = pool
.iter()
.filter_map(|(_, bytes)| {
let tx = parse_pool_tx(bytes)?;
let fee: u64 = tx
.output_candidates
.iter()
.filter(|c| {
c.ergo_tree_bytes() == ergo_mempool::validator::MAINNET_FEE_PROPOSITION_BYTES
})
.map(|c| c.value)
.sum();
if fee == 0 {
return None;
}
let size = bytes.len() as u64;
if size == 0 {
return None;
}
Some(PoolFeeEntry {
fee,
fee_per_byte: fee / size,
})
})
.collect();
entries.sort_by_key(|e| std::cmp::Reverse(e.fee_per_byte));
entries
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether ergo_mempool::validator exposes a testnet-specific fee proposition constant,
# and whether the network is threaded into fee-related pool ranking anywhere.
rg -n 'FEE_PROPOSITION_BYTES' --type=rust -C2
rg -n 'MAINNET_FEE_PROPOSITION_BYTES|TESTNET_FEE_PROPOSITION_BYTES' -g '!target' -C3

Repository: arkadianet/ergo

Length of output: 240


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- pool_fee_stats.rs ---'
sed -n '1,220p' ergo-node/src/api_bridge/scala_compat/pool_fee_stats.rs

echo
echo '--- search fee proposition constants ---'
rg -n 'FEE_PROPOSITION|fee proposition|miners fee|MAINNET_FEE' ergo-node -g '*.rs' -C 2

echo
echo '--- search network branching in scala_compat ---'
rg -n 'static_cfg\.network|testnet|mainnet' ergo-node/src/api_bridge/scala_compat -g '*.rs' -C 2

echo
echo '--- bridge construction / call sites ---'
rg -n 'ScalaCompatBridge|pool_fee_histogram|pool_recommended_fee|pool_expected_wait_time_ms' ergo-node -g '*.rs' -C 2

Repository: arkadianet/ergo

Length of output: 19875


🌐 Web query:

Ergo testnet fee proposition bytes MAINNET_FEE_PROPOSITION_BYTES TESTNET_FEE_PROPOSITION_BYTES sigma-rust MINERS_FEE_TESTNET_ADDRESS

💡 Result:

In the Ergo platform, transaction fees are collected in specialized output boxes that are spendable only by miners [1][2]. These boxes are defined by a specific script, often referred to as the fee proposition [1]. Regarding your specific search terms: 1. MAINNET_FEE_PROPOSITION_BYTES / TESTNET_FEE_PROPOSITION_BYTES: These are not standard, globally defined constants exposed in the public API or primary documentation [1][3]. Instead, the fee proposition is derived dynamically from the node's chain settings [1]. The node implementation calculates the fee by filtering transaction outputs that match the ErgoTree defined in settings.chainSettings.monetary.feeProposition [1]. This contract typically allows the miner to spend the box provided they satisfy a specific condition (e.g., proving ownership of the miner's public key) [1]. 2. sigma-rust: This is the official Rust library for Ergo, which includes essential tools for transaction building, fee estimation, and wallet interactions [4]. It provides functionality such as TxBuilder::estimate_tx_size_bytes() and constants like SUGGESTED_TX_FEE to help developers estimate and construct transactions appropriately [5]. 3. MINERS_FEE_TESTNET_ADDRESS: There is no fixed "miner's fee address" in Ergo [1]. Because Ergo uses an eUTXO model, fees are not sent to a static wallet address but are instead included as output boxes within the transaction itself [6][2]. These output boxes are "protected" by the miner's fee script [1]. If you are interacting with the testnet, the miner reward is simply handled by the protocol based on the coinbase transaction of a block; for custom fee payments within your own transactions, you must ensure the fee output box uses the script that the network expects to be spent by miners [1][6]. For developers, the standard practice is to use the Ergo Node API or the sigma-rust SDK to handle fee output creation, rather than hardcoding proposition bytes, as these can change based on network upgrades (soft forks) or protocol configurations [1][7]. Developers can query their local node to understand current fee requirements or use SDK methods to suggest appropriate fees based on transaction size or validation cost [5][2].

Citations:


🌐 Web query:

site:github.com MINERS_FEE_TESTNET_ADDRESS ergo

💡 Result:

In the Ergo blockchain ecosystem, MINERS_FEE_TESTNET_ADDRESS is typically a configuration constant or reference point used by developers and automated tools (such as bots or wallet libraries) to facilitate transaction fee payments on the testnet [1][2]. There is no single "universal" static testnet miner fee address because mining rewards and transaction fees are distributed to the address configured by the miner running the node or mining pool [3][4]. However, developers often use specific addresses for testing purposes in their implementations: 1. Software Configuration: In projects like ergo-appkit or dex-backends, developers often configure their own wallet address (derived from a mnemonic seed) to receive fees and pay transaction costs [1][4]. 2. Code Constants: Historical references to specific fee addresses sometimes appear in repository code (such as in ergo-appkit or specific protocol prototypes) for testing [5][2]. These are often subject to change, as evidenced by release notes indicating that testnet fee addresses have been updated or fixed in library versions [2]. If you are a developer looking for the current address to use in your software, you should: - Avoid hardcoding a specific address if possible; instead, derive the address from your test wallet mnemonic as described in your project's configuration files [1][4]. - Check the current documentation or configuration schema of the specific tool or SDK you are using (e.g., ergo-appkit), as these addresses are managed at the implementation level rather than being a global blockchain constant [4][2]. - Use the Ergo Explorer (testnet) to verify addresses and monitor testnet activity [5][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- fee proposition config / chain settings ---'
rg -n 'feeProposition|fee proposition|monetary\.fee|miner fee|fee_tree_bytes|fee_ergo_tree' ergo-node -g '*.rs' -C 3

echo
echo '--- ScalaCompatBridge methods around pool fee routes ---'
sed -n '620,720p' ergo-node/src/api_bridge/scala_compat/mod.rs

echo
echo '--- ScalaCompatBridge constructor / network field ---'
sed -n '1,120p' ergo-node/src/api_bridge/scala_compat/mod.rs

echo
echo '--- any testnet fee constant in repo ---'
rg -n 'TESTNET_FEE_PROPOSITION_BYTES|fee proposition bytes.*testnet|testnet.*fee proposition' -g '*.rs' ergo-node ergo-*/src -C 2

Repository: arkadianet/ergo

Length of output: 16774


Use the active network’s fee proposition bytes here. rank_pool_by_fee_per_byte hardcodes MAINNET_FEE_PROPOSITION_BYTES (ergo-node/src/api_bridge/scala_compat/pool_fee_stats.rs:57-87), but the bridge already handles mainnet/testnet elsewhere. On testnet this will treat every pool tx as zero-fee and break the fee histogram/recommendation endpoints.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ergo-node/src/api_bridge/scala_compat/pool_fee_stats.rs` around lines 57 -
87, The rank_pool_by_fee_per_byte function must use the active network’s fee
proposition bytes instead of MAINNET_FEE_PROPOSITION_BYTES. Thread or obtain the
existing network configuration used by the bridge, select its mainnet/testnet
fee proposition bytes, and use that value when filtering transaction outputs
while preserving the current ranking behavior.

Comment thread ergo-node/src/node/messaging/dispatch.rs Outdated
Comment thread ergo-node/src/node/messaging/dispatch.rs
Comment thread ergo-node/src/node/messaging/popow.rs Outdated
Comment thread ergo-node/src/node/snapshot_emit/bootstrap_panel.rs Outdated
Comment thread ergo-node/src/node/snapshot_emit/mod.rs Outdated
Comment on lines +78 to +117
let (best_header_parent, best_header_ts, best_header_score) = state
.store
.get_header_meta(&cs_best_header_id)
.ok()
.flatten()
.map(|m| (m.parent_id, m.timestamp, m.cumulative_score))
.unwrap_or(([0u8; 32], 0, Vec::new()));
let (best_full_parent, best_full_ts, best_full_block_score) = state
.store
.get_header_meta(&cs_best_full_block_id)
.ok()
.flatten()
.map(|m| (m.parent_id, m.timestamp, m.cumulative_score))
.unwrap_or(([0u8; 32], 0, Vec::new()));
// Difficulty surface: HeaderMeta doesn't carry `n_bits`, so deserialize
// each tip header for it. Height 0 = no tip yet (legit 0); for a real
// tip a failure is a store/serializer fault, logged with its cause, and
// `None` tells the publisher to carry the last-known value forward
// (never a synthetic 0).
let best_header_n_bits = read_tip_n_bits(
state,
&cs_best_header_id,
cs_best_header_height,
"best_header",
);
let best_full_block_n_bits = read_tip_n_bits(
state,
&cs_best_full_block_id,
cs_best_full_block_height,
"best_full_block",
);
// Ergo genesis is height 1 in HEADER_CHAIN_INDEX (h=0 is never written —
// see `rewrite_best_chain_into_index` walk terminating at cur_height == 1).
// Matches Scala's chain numbering.
let genesis_block_id = state
.store
.get_header_id_at_height(1)
.ok()
.flatten()
.unwrap_or([0u8; 32]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Avoid publishing synthetic chain metadata on read failures.

Lines 78-117 collapse both storage errors and absent records into zero parent IDs, timestamps, scores, and genesis IDs while retaining live tip heights and IDs. This creates an internally inconsistent snapshot, and downstream /info consumers use these values without fallback.

Handle the empty-chain case separately; for a live chain, log the fault and retain the previous snapshot or skip this publication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ergo-node/src/node/snapshot_emit/mod.rs` around lines 78 - 117, Update the
snapshot metadata assembly around get_header_meta and genesis_block_id so
empty-chain state is handled explicitly, while live-chain lookup failures are
treated as publication failures. Do not replace missing or errored records with
zero IDs, timestamps, scores, or genesis IDs when live tip heights exist; log
the storage fault and retain the previous snapshot or skip publication. Preserve
the existing read_tip_n_bits behavior and valid metadata for successful lookups.

Comment thread ergo-node/src/snapshot/publisher.rs Outdated
Comment thread ergo-node/src/snapshot/publisher.rs
arkadianet and others added 5 commits July 17, 2026 13:14
Flagged in PR review, both pre-existing (unmodified by the messaging.rs
split) and both narrowly scoped to this file:

- GetNipopowProof serving called `state.store.as_utxo().expect(...)`
  after only ruling out PoPowSparse header availability, which doesn't
  by itself prove the backend is UTXO. Decline gracefully (empty
  response) instead of asserting the combination can never reach here
  from peer input.
- The tx-typed CODE_MODIFIER branch called
  `coordinator.on_transaction_received(peer, &mod_id)` trusting the
  peer's claimed `mod_id` without verifying it against the delivered
  bytes -- unlike the section-type branch just below it, which already
  has an analogous `verify_section_modifier_id` check. A peer
  substituting different bytes under a requested id could poison the
  delivery tracker's bookkeeping (clearing `late_acceptable` for the
  REAL awaited tx) even though `admit_transaction` itself derives its
  own tx_id from the bytes and stays correct either way. Added
  `claimed_tx_id_matches` (parse + `transaction_id` recompute +
  compare) and penalize-and-skip on mismatch, mirroring the existing
  section-type pattern. Covered with 3 focused unit tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi
…nly after verification

Flagged in PR review, pre-existing behavior unmodified by the messaging.rs
split: the ERGO_CAPTURE_NIPOPOW_PROOF one-shot capture hook fired right
after the outer wire-frame parse, before the proof structure was even
parsed and before the reducer's genesis/validation check ran -- a
malformed or wrong-genesis proof could get written to disk and mistaken
for a clean Scala-served fixture.

Moved the capture into the BetterChain/NoBetterChain result arms, i.e.
after wire-frame parse, structure parse, AND the reducer/verifier's
genesis + validation checks all succeed. Factored into
maybe_capture_verified_proof to avoid duplicating the write logic across
both arms.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi
…post-install catch-up panel

Flagged in PR review, pre-existing behavior unmodified by the snapshot
split: the Mode 2 post-install catch-up branch of the bootstrap dashboard
reported snapshot_height as the LIVE best_full_block_height and
manifest_id as None -- but best_full_block_height keeps climbing every
tick as catch-up applies blocks, so the dashboard's "snapshot height"
field misreported a live, advancing value as if it were the fixed height
the snapshot was actually installed at. manifest_id was always None
because the snapshot-bootstrap reducer clears its own copy post-install.

Added NodeState.installed_snapshot: Option<(u32, [u8; 32])>, latched by
install_reconstructed_snapshot at the moment install succeeds -- before
the reducer clears its copy. build_bootstrap_status's in_catchup branch
now reads this fixed value instead of the advancing best_full, falling
back to the prior best-effort proxy only in the (should-be-unreachable)
case where post_install_catchup is true but the field was never set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi
…okups

Flagged in PR review, pre-existing behavior unmodified by the snapshot
split: the tip header_meta lookups (best_header/best_full_block
parent_id/timestamp/cumulative_score) and the genesis_block_id lookup all
silently fell back to the same zeroed shape on ANY failure -- both the
legitimate "height 0, nothing committed yet" case and a genuine store
fault on a live, non-empty chain. The latter would be indistinguishable
from the former to an operator watching the dashboard: a real storage
problem reads identically to "chain hasn't started yet".

Added header_meta_or_default (mirroring read_tip_n_bits's existing
height-0-vs-real-fault distinction, right above it in this same file) and
an explicit height-gated match for genesis_block_id, both now logging at
error level when height > 0 but the lookup misses or errors. The
fallback VALUE is unchanged (still zeroed) -- extending these fields to
the full carry-forward-or-skip-publish treatment n_bits already has
(see the companion publisher.rs fix) would mean threading Option through
SnapshotParts/NodeSnapshot/build_snapshot for seven more fields, a
separate, larger piece of work better done as its own dedicated pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi
… rollback

Flagged in PR review, pre-existing behavior unmodified by the snapshot
split, two related correctness bugs in the same publish() method:

1. n_bits carry-forward on read failure only checked
   `height > 0 && prior_n_bits == 0` before reusing the prior snapshot's
   difficulty -- it never compared the CURRENT tip id against the PRIOR
   snapshot's tip id. A read failure on a tip that had already advanced
   to a genuinely different block would silently relabel the OLD tip's
   difficulty as belonging to the NEW one. Now compares
   hex::encode(parts.best_header_id)/best_full_block_id against the
   prior snapshot's stored tip id; carry-forward only fires when the
   tip is unchanged, otherwise the publish is skipped (retries next
   tick) exactly as the existing "no real prior to carry" case already
   does.

2. The stall-clock baselines (last_header_height/last_full_block_height)
   only ever moved forward. A reorg dropping the height below the
   last-observed value left the baseline pinned at the pre-reorg height,
   so re-applying blocks back through the rolled-back range never
   registered as "advanced" relative to the stale baseline --
   last_block_progress_at would sit frozen for the whole recovery
   replay, and an actively, successfully recovering node could
   misreport itself as stalled if that replay took longer than
   STALL_THRESHOLD_SECS. Now resets the baseline DOWN when a rollback
   is detected, before the advanced checks run, so recovery progress
   is recognized again.

Updated the existing carry-forward test to use explicit matching tip
ids (documenting the same-tip case it actually exercises), and added
two new tests: one proving a different-tip read failure skips publish
rather than mislabeling stale difficulty, one proving rollback-recovery
progress refreshes the stall clock.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi
@arkadianet

Copy link
Copy Markdown
Owner Author

Went through the review findings against current code. Verified all 8 by diffing against the pre-refactor originals — every one describes pre-existing behavior, byte-identical before and after this PR's mechanical moves. None were introduced by the split itself.

Fixed (7, pushed as new commits 4a0abf3, 56d589f, 430975a, 81ad88f, ebfce45):

  • dispatch.rs NiPoPoW proof serving: as_utxo().expect(...) after only ruling out PoPowSparse header availability doesn't actually prove the backend is UTXO. Now declines gracefully instead of asserting the combination can't reach here from peer input.
  • dispatch.rs tx-modifier delivery tracking: the tx-typed CODE_MODIFIER branch trusted the peer's claimed mod_id without verifying it against the delivered bytes, unlike the section-type branch just below it (which already has verify_section_modifier_id). A peer substituting different bytes under a requested id could poison the delivery tracker's bookkeeping (clearing late_acceptable for the real awaited tx) even though admit_transaction itself stays correct (it derives its own tx_id from the bytes). Added claimed_tx_id_matches + penalize-and-skip on mismatch, with 3 focused unit tests.
  • popow.rs Scala-oracle capture ordering: the ERGO_CAPTURE_NIPOPOW_PROOF one-shot capture fired right after the wire-frame parse — before structure parsing and before the reducer's genesis/validation check. Moved into the BetterChain/NoBetterChain result arms so a malformed or wrong-genesis proof is never captured as a "clean" fixture.
  • bootstrap_panel.rs post-install snapshot metadata: the catch-up branch reported snapshot_height as the live, advancing best_full_block_height instead of the fixed height the snapshot was actually installed at (and manifest_id as always-None, since the reducer clears its own copy post-install). Added NodeState.installed_snapshot, latched by install_reconstructed_snapshot at the moment install succeeds, before the reducer's own copy clears.
  • snapshot_emit/mod.rs header-meta/genesis fault masking: tip header_meta lookups and the genesis-id lookup silently returned the same zeroed shape on both "height 0, legitimately nothing committed" and "genuine store fault on a live chain" — indistinguishable to an operator. Added explicit height-gated error logging (mirroring read_tip_n_bits's existing convention right above these call sites) so a real fault is no longer silently mistaken for an empty chain. See note below on scope.
  • publisher.rs n_bits carry-forward (two bugs in the same method): (1) carry-forward on a read failure only checked height > 0 && prior_n_bits == 0, never comparing the current tip id against the prior snapshot's tip id — so a read failure on a tip that had already advanced to a genuinely different block would relabel the OLD tip's difficulty as belonging to the new one. Now compares tip ids; skips publish (retries next tick) when they differ. (2) the stall-clock baselines only ever moved forward, so a reorg dropping the height below the last-observed value pinned the baseline at the pre-reorg height — re-applying blocks back through the rolled-back range never registered as progress, and a node actively, successfully recovering from a reorg could misreport itself as stalled. Now resets the baseline down when a rollback is detected. Added 2 new tests plus updated the existing carry-forward test to make its same-tip assumption explicit.

Partially addressed, not fully — flagged for scope reasons:

  • snapshot_emit/mod.rs: the finding also asked to "retain the previous snapshot or skip publication" for the header-meta/genesis fields, matching n_bits' existing carry-forward architecture (which I did fully fix above, since it was an existing bug in an existing mechanism). Extending that same architecture to 7 more fields (best_header_parent_id, best_header_timestamp_ms, best_header_score, the same trio for full_block, plus genesis_block_id) means threading Option through SnapshotPartsbuild_snapshotNodeSnapshot, with matching carry-forward/skip-publish resolution logic per field. That's a real, coherent piece of work — just a much larger one than a logging fix, and it deserves its own dedicated pass (with its own test coverage per field) rather than a rushed partial version bundled into this response. Logging now means the fault is at least no longer silently invisible.

Skipped (1) — not valid:

  • pool_fee_stats.rs "must use the active network's fee proposition bytes": this finding assumes a per-network fee-proposition tree exists to select between, but it doesn't. MAINNET_FEE_PROPOSITION_BYTES is Ergo's canonical, network-independent fee-proposition ErgoTree (proveDlog(minerPk) ∧ HEIGHT >= SelfCreationHeight + 720, constant-segregated) — the doc comment in ergo-mempool/src/validator.rs states outright that "const bytes are stable across versions", and it's the exact same constant used unconditionally by the mempool's own consensus-critical admission validator, the miner's coinbase construction, and the wallet's fee-tree usage, on every network this node supports. There is no TESTNET_FEE_PROPOSITION_BYTES anywhere in the codebase, and none of those other call sites branch on network either. Threading a "network-aware" selection into pool_fee_stats.rs alone would make it the ONE place in the codebase that disagrees with what the mempool itself considers "the fee" for a given tx.

Full gate re-verified after all fixes: cargo test -p ergo-node (505 lib tests incl. 6 new + full integration suite), cargo clippy -p ergo-node --all-targets --all-features -- -D warnings clean, cargo fmt --all -- --check clean, plus whole-workspace cargo clippy --workspace --all-targets --all-features -- -D warnings and cargo test --workspace both clean. Also did a manual boot smoke test against real testnet peers (5188 headers synced, no panics, clean SIGTERM shutdown) to sanity-check the messaging-path changes.

@arkadianet

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Resolves the modify/delete conflict on ergo-node/src/node/boot.rs: PR #205
(merged to main) split boot.rs into boot/{mod,peers,sync_setup,mining,
api_wiring}.rs; this branch's earlier review-response commit had added
NodeState::installed_snapshot's initializer to the old monolithic
boot.rs. Ported that one-line addition (installed_snapshot: None,) to the
corresponding NodeState-construction site in the new boot/mod.rs.

Everything else merged cleanly (zero file overlap between the two split
PRs by design).
@arkadianet

Copy link
Copy Markdown
Owner Author

Resolved the merge conflict with `main` (`bfa3e66`): #205 merged first and its split of `boot.rs` → `boot/{mod,peers,sync_setup,mining,api_wiring}.rs` collided with this branch's earlier `NodeState::installed_snapshot` initializer, which had been added to the old monolithic `boot.rs`. Ported that one-line addition to the corresponding spot in the new `boot/mod.rs`; everything else merged cleanly (the two split PRs touch disjoint files by design). Re-ran the full gate post-merge: `cargo test -p ergo-node` (506 passed incl. #205's own review-fix test + full integration suite), `cargo clippy -p ergo-node --all-targets --all-features -- -D warnings` clean, `cargo fmt --all -- --check` clean. PR now shows clean/mergeable.

Re-verified all 8 inline findings against current code — these are the same findings from the previous review round, and all the fixable ones were already addressed in commits `4a0abf3`/`56d589f`/`430975a`/`81ad88f`/`ebfce45` (still intact post-merge, confirmed above):

  • `pool_fee_stats.rs` network-aware fee bytes: still not valid — no per-network fee-proposition constant exists anywhere in the codebase (re-checked `ergo-mempool`/`ergo-chain-spec`/`ergo-validation`); `MAINNET_FEE_PROPOSITION_BYTES` is the same network-independent constant the consensus-critical mempool validator itself uses unconditionally. Same reasoning as before, unchanged.
  • `dispatch.rs` NiPoPoW `as_utxo()` panic and tx-modifier id verification: already fixed.
  • `popow.rs` capture-hook ordering: already fixed.
  • `bootstrap_panel.rs` snapshot metadata: already fixed via `NodeState::installed_snapshot`.
  • `snapshot_emit/mod.rs` header-meta/genesis fault masking: partially addressed (explicit error logging added) — the full "retain previous snapshot or skip publication" ask is still flagged as separate follow-up work (threading `Option` through `SnapshotParts`/`NodeSnapshot` for 7 more fields), same as noted previously.
  • `publisher.rs` n_bits carry-forward + rollback stall-baseline: already fixed, with regression tests.

No new changes needed this round beyond the merge conflict resolution.

@arkadianet
arkadianet merged commit 981c5a0 into main Jul 17, 2026
9 checks passed
@arkadianet
arkadianet deleted the refactor/ergo-node-split-2-snapshot-messaging branch July 17, 2026 06:14
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.

1 participant