Skip to content

Conversation

@etorreborre
Copy link
Contributor

@etorreborre etorreborre commented Jan 22, 2026

This PR completes (on the responder side) and tests the retrieval of blocks with the blockfetch protocol:

  • The streaming of blocks with the StartBatch / Block / BatchDone messages.
  • The retrieval of a range of blocks from the ChainStore.
  • The test the blockfetch protocol works when an initiator and a responder are connected over TCP.
  • The test that missing blocks in a request range won't prevent other blocks to be fetched.

Summary by CodeRabbit

  • New Features

    • Block range querying to retrieve contiguous header sequences.
    • Block streaming with batched, range-based fetching and explicit send/done signaling.
  • Improvements

    • Clearer network error context when reading messages.
    • Running-task handle is now clonable for easier task management.
    • Responder/initiator flow enhanced for coordinated, batched block transfer.
  • Tests

    • Added tests covering chain-range semantics and end-to-end batched block-fetch.

✏️ Tip: You can customize this high-level summary in your review settings.

@etorreborre etorreborre self-assigned this Jan 22, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 22, 2026

Walkthrough

Adds ReadOnlyChainStore::get_range for inclusive parent-linked header traversal, implements responder-side block streaming (BlockStreaming, current_range, load_first_block), updates related exports/types and tests, and adds a RocksDB unit test for get_range.

Changes

Cohort / File(s) Summary
Trait Definition
\crates/amaru-ouroboros-traits/src/stores/consensus/mod.rs``
Added fn get_range(&self, from_inclusive: &HeaderHash, to_inclusive: &HeaderHash) -> Vec<HeaderHash> to ReadOnlyChainStore to walk parent links and return an inclusive ordered Vec of header hashes.
BlockFetch Responder
\crates/amaru-protocols/src/blockfetch/responder.rs``
Added public BlockStreaming enum (SendBlock(Vec<u8>), Done), current_range: VecDeque<HeaderHash>, load_first_block helper; changed local/network handling to populate/stream ranges or signal done.
BlockFetch Module & Exports
\crates/amaru-protocols/src/blockfetch/mod.rs``
Re-exported BlockStreaming; changed register_blockfetch_responder return type from StageRef<Void> to StageRef<BlockStreaming>.
Connection Types
\crates/amaru-protocols/src/connection.rs``
Updated StateResponder::blockfetch_responder field type from StageRef<Void> to StageRef<BlockStreaming>.
Mux Error Context
\crates/amaru-protocols/src/mux.rs``
Minor wording tweak in network read error context.
Tests & Stores
\crates/amaru-protocols/src/tests.rs`, \crates/amaru-stores/src/rocksdb/consensus/mod.rs``
Reworked tests to exercise batched block fetches/coordination; added RocksDB unit test get_range_returns_a_list_of_linked_header_hashes.
Runtime Utility
\crates/pure-stage/src/tokio.rs``
TokioRunning now derives Clone (#[derive(Clone)]).

Sequence Diagram(s)

sequenceDiagram
    participant Net as Network
    participant Resp as BlockFetchResponder
    participant Store as ReadOnlyChainStore

    Net->>Resp: RequestRange(from, through)
    activate Resp
    Resp->>Store: get_range(from, through)
    activate Store
    Store-->>Resp: Vec<HeaderHash>
    deactivate Store

    Resp->>Resp: current_range = VecDeque(hashes)
    Resp->>Resp: load_first_block()
    alt block available
        Resp->>Store: load block bytes for header
        activate Store
        Store-->>Resp: RawBlock bytes
        deactivate Store
        Resp-->>Net: BlockStreaming::SendBlock(bytes)
        Resp->>Resp: continue streaming or emit Done
    else no block
        Resp-->>Net: BlockStreaming::Done / NoBlocks
    end
    deactivate Resp
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • abailly
  • jeluard

Poem

A trail of headers, neat and bright,
the responder queues blocks into the night,
parents link like beats in a tune,
RocksDB hums — sync done by noon,
grab a flat white, celebrate the flight.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main purpose of the PR—completing the block fetch protocol implementation with streaming, range retrieval, and protocol tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

@etorreborre
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 22, 2026

✅ Actions 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@crates/amaru-ouroboros-traits/src/stores/consensus/mod.rs`:
- Around line 96-116: The get_range implementation can return a partial path
when the walk hits a root or a missing parent before reaching from_inclusive;
update get_range (and its use of load_header and header.parent()) to return an
empty Vec if the ancestry chain breaks before reaching from_inclusive: while
walking from to_inclusive toward from_inclusive, detect any missing header
(load_header returns None) or missing parent that prevents reaching
from_inclusive and immediately return Vec::new(); only if the loop finishes with
current_hash == *from_inclusive should you collect and reverse the headers and
return them.

In `@crates/amaru-protocols/src/blockfetch/responder.rs`:
- Around line 54-56: Update the docstring for the function that "loads the first
existing block in the current range" to accurately reflect the loop behavior:
replace the sentence that says "the first element of the current_range is
consumed" with wording that clarifies multiple elements may be consumed (e.g.,
"elements of current_range are consumed until a block is found or the range is
exhausted"), keeping the rest of the description unchanged; locate the doc
comment immediately above the block-loading function in responder.rs to apply
this change.

In `@crates/amaru-protocols/src/tests.rs`:
- Around line 65-69: The test soak was reduced to 3s causing flakiness; restore
the original 5s delay by changing the sleep in the tokio::select! branch from
Duration::from_secs(3) back to Duration::from_secs(5) so the responder.join() /
initiator.join() stability check retains the intended buffer.
🧹 Nitpick comments (1)
crates/amaru-protocols/src/blockfetch/responder.rs (1)

95-107: Avoid block shadowing for clarity.
The inner block hides the outer block, which makes the flow a bit harder to follow. Renaming keeps things crystal clear.

🎬 Tiny readability tweak
-                if let Some(block) = self.load_first_block(eff).await {
+                if let Some(next_block) = self.load_first_block(eff).await {
                     eff.send(
                         eff.me_ref(),
-                        Inputs::Local(BlockStreaming::SendBlock(block.to_vec())),
+                        Inputs::Local(BlockStreaming::SendBlock(next_block.to_vec())),
                     )
                     .await;
                 } else {

@etorreborre etorreborre force-pushed the etorreborre/feat/block-fetch-test branch from 592f873 to 659047b Compare January 22, 2026 15:47
@etorreborre etorreborre marked this pull request as ready for review January 22, 2026 15:50
@codecov
Copy link

codecov bot commented Jan 22, 2026

Codecov Report

❌ Patch coverage is 94.11765% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/amaru-protocols/src/blockfetch/responder.rs 91.83% 4 Missing ⚠️
...amaru-ouroboros-traits/src/stores/consensus/mod.rs 94.11% 1 Missing ⚠️
Files with missing lines Coverage Δ
crates/amaru-protocols/src/blockfetch/mod.rs 88.73% <100.00%> (ø)
crates/amaru-protocols/src/connection.rs 94.26% <ø> (+4.45%) ⬆️
crates/amaru-protocols/src/mux.rs 94.02% <100.00%> (ø)
crates/amaru-stores/src/rocksdb/consensus/mod.rs 88.78% <100.00%> (+0.24%) ⬆️
crates/pure-stage/src/tokio.rs 89.38% <ø> (ø)
...amaru-ouroboros-traits/src/stores/consensus/mod.rs 57.95% <94.11%> (+8.65%) ⬆️
crates/amaru-protocols/src/blockfetch/responder.rs 92.85% <91.83%> (+20.79%) ⬆️

... and 20 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@etorreborre etorreborre requested a review from rkuhn January 22, 2026 17:11
@etorreborre etorreborre force-pushed the etorreborre/feat/block-fetch-test branch from 659047b to 4b99e80 Compare January 23, 2026 08:55
Signed-off-by: etorreborre <etorreborre@yahoo.com>
@etorreborre etorreborre force-pushed the etorreborre/feat/block-fetch-test branch from 4b99e80 to 823d0cb Compare January 23, 2026 09:12
Copy link
Contributor

@rkuhn rkuhn left a comment

Choose a reason for hiding this comment

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

nice! just some minor improvements and a possible flakiness issue

fn get_range(&self, from_inclusive: &HeaderHash, to_inclusive: &HeaderHash) -> Vec<HeaderHash> {
let mut headers = vec![];
let mut current_hash = *to_inclusive;
while current_hash != *from_inclusive {
Copy link
Contributor

Choose a reason for hiding this comment

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

This loop will run until genesis if from_inclusive is not an ancestor of to_inclusive — this is an attack vector. We should probably set an upper bound on how many hashes are returned at maximum. What does the Haskell node do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This one is a naive strategy indeed. The Haskell node returns an iterator on blocks when they are on the best chain, only if from starts before the chain anchor (before k blocks). Then, there's no limitation that I can see. Otherwise we are on a fork and the amount of streamed blocks is limited.

I propose to tackle this in a follow-up PR if that's ok with you.

State::Idle,
Self {
muxer,
current_range: VecDeque::default(),
Copy link
Contributor

Choose a reason for hiding this comment

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

since we only consume from one end and don’t add to the other, perhaps a Vec is enough (with parent after child, so we can pop off the end)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This might go away entirely in a follow-up PR.

let store = Store::new(eff.clone());
let range = store.get_range(&from.hash(), &through.hash());
self.current_range = VecDeque::from(range);
if let Some(block) = self.load_first_block(eff).await {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be cleaner and easier to read if there was a check_block_exists function to decide this case here and then only have a Streaming message without further content that is used in local() to send the next block or end the stream. Your proposal is correct, though, and avoids one call to the database. I’m on the fence — have you tried the other way to see which one you like better?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I haven't tried both, but I indeed wanted to avoid one call.

}
IntersectFound(point, tip) => {
tracing::info!(peer = %msg.peer, ?point, ?tip, "intersect found");
tracing::info!(peer = %msg.peer, %point, %tip, "intersect found");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
tracing::info!(peer = %msg.peer, %point, %tip, "intersect found");
tracing::info!(peer = %msg.peer, %point, tip_point = %tip.point(), "intersect found");

The issue with %tip is that it doesn’t show the slot number, which makes it harder to see the relation to %point.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why don't we change the Tip Display instance to include the slot?

chain_store.set_best_chain_hash(&header.hash())?;

// Add a block for each header
// We skip one block to test that the initiator can try to fetch missing blocks
Copy link
Contributor

Choose a reason for hiding this comment

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

this is unclear to me: it was my understanding that a range can only be returned if all blocks are present, otherwise NoBlocks. did I read wrong?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was my impression that we could start streaming from several peers at the same time, where some peers might not yet have all the blocks on the required chain. So we respond in a best-effort manner. This is definitely something that we could contribute to the blueprint because the specification is not clear-cut on the subject.

Signed-off-by: etorreborre <etorreborre@yahoo.com>
@etorreborre etorreborre force-pushed the etorreborre/feat/block-fetch-test branch 2 times, most recently from f668d9c to e95cb50 Compare January 26, 2026 12:41
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@crates/amaru-protocols/src/tests.rs`:
- Around line 146-156: The current wait_for_termination function drops the
Result from tokio::time::timeout so a timeout is ignored; update
wait_for_termination to check the timeout result and return an error when the
timeout elapses (e.g., match or use ? on the timeout call and convert the
Elapsed into an anyhow::Error), while still awaiting responder_done.notified()
and initiator_done.notified(); refer to the function name wait_for_termination
and the notifier variables responder_done and initiator_done to locate where to
perform the change.
🧹 Nitpick comments (1)
crates/amaru-protocols/src/tests.rs (1)

195-258: Public API surface is a bit mismatched.
AcceptState is public and accept_stage is public, but external callers can’t construct AcceptState because new is private and fields aren’t public. Either make the constructor public or reduce visibility to pub(crate) to avoid a “can’t call this” API.

🎛️ Option A: make the constructor public
-impl AcceptState {
-    fn new(manager_stage: StageRef<ManagerMessage>, notify: Arc<Notify>) -> Self {
+impl AcceptState {
+    pub fn new(manager_stage: StageRef<ManagerMessage>, notify: Arc<Notify>) -> Self {
         Self {
             manager_stage,
             notify,
         }
     }
 }

…he responder are done

Signed-off-by: etorreborre <etorreborre@yahoo.com>
@etorreborre etorreborre force-pushed the etorreborre/feat/block-fetch-test branch from e95cb50 to 01c80f9 Compare January 26, 2026 13:00
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.

3 participants