Skip to content

Two bug fixes - #69

Open
gdeest wants to merge 10 commits into
mainfrom
fix/gsa-functional-issues
Open

gdeest wants to merge 10 commits into
mainfrom
fix/gsa-functional-issues

Conversation

@gdeest

@gdeest gdeest commented May 7, 2026 •

Copy link
Copy Markdown
Contributor

This PR introduces two fixes surfaced during mainnet benchmarking:

  1. Stream-bound entries should be picked by (slot, hash), not position.

checkEntryMatch first picked a candidate block by position then checked the hash. The problem is that blocks can share a slot in the very special case of Byron EBBs, where the first slot of each epoch contains a two blocks. This lead to StreamBoundNotFound errors when the block being picked was not the one requested.

  1. Properly validate ChainSync intersection candidates against the on-disk chain.

When syncing from an empty chain, cardano-node actually sends two candidates:

a) A synthetic anchor computed from Origin and genesis.json file.
b) The Genesis origin point.

I am not exactly sure what the rationale is behind this, but we would previously accept the synthetic anchor without checking, which would later result in an error when the node tries to fetch that anchor.

Two changes that together unblock real-mainnet sync through GSA.

1) Stream-bound entries are picked by (slot, hash), not position.

   `checkEntryMatch` was a positional pick (listToMaybe / last) followed
   by a hash check. That's wrong when multiple entries share a slot —
   the canonical case being the Byron EBB and the first main block of
   an epoch. A range request for the EBB alone landed on the main
   block, failed the hash check, and surfaced as `StreamBoundNotFound`;
   the symptom on a fresh-node mainnet sync was BlockFetch sending
   `MsgStartBatch` then going silent.

   Replaced with `dropUntilLowerBound` / `truncateAtUpperBound`, which
   scan by (slot, hash). Both remember the most recent same-slot
   non-match so `StreamBoundNotFound` still carries the near-miss entry
   (preserves behaviour expected by
   `prop_onDemandIteratorFromErrorsWhenStartingWithSlotNumberOnChainButWrongHeaderHash`).

2) ChainSync intersection candidates are probed against the on-disk
   chain.

   `cardano-node` 10.6.x emits a synthetic slot-0 hash in
   `MsgFindIntersect` that doesn't match the Byron EBB on disk. The old
   slot-only `withinTip` check accepted it, the iterator failed the
   hash equality, and the responder died. `findUsableIntersection` now
   opens an iterator on each candidate to confirm it's on our chain;
   non-matches are rejected and the loop tries the next candidate, so
   the bootstrap pattern of bogus-block-then-GenesisPoint correctly
   falls back to Origin. Same idiom as ouroboros-consensus's
   `Impl.Follower.forward`, which uses `streamAfterPoint` as its
   on-chain test.
@gdeest
gdeest force-pushed the fix/gsa-functional-issues branch from 34d228e to 6982de5 Compare May 7, 2026 15:03
gdeest and others added 8 commits May 7, 2026 23:19
Add unit tests covering the Byron EBB / first-main-block collision: the
scenario where two entries occupy the same slot and the bound helpers
must distinguish them by hash rather than position.

These would have failed against the previous positional 'checkEntryMatch'
logic.

@vreuter vreuter 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.

Really nice! Thank you @gdeest
I suggest a few things here and in #74 , but very close to merge-ready I think.

(getE es)
dropUntilLowerBound ci fromSlot fromHash = go Nothing
where
-- 'mbNear' = most recent same-slot non-match, reported in StreamBoundNotFound.

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.

Suggested change
-- 'mbNear' = most recent same-slot non-match, reported in StreamBoundNotFound.
-- 'mbNear' = most recently processed same-slot non-match, to report in StreamBoundNotFound.

This way, it's clearer that the notion of recency applies to the algorithmic iteration, rather than somewhat suggesting that it's more recent on the blockchain (in the sense that a slot is an atomic unit in time, and therefore things in the same slot cannot be more or less recent than one another). The other small change here ("to report in") I think makes it clearer that the last slot of StreamBoundNotFound won't always be an entry from the requested slot, but rather could be something from another slot. Small points, but I think worth the precision in the wording. WDYT?

Either (IllegalStreamOperation blk) [SizedEntry blk]
truncateAtUpperBound ci toSlot toHash = go [] Nothing
where
-- 'mbNear' = most recent same-slot non-match (see 'dropUntilLowerBound').

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.

Suggested change
-- 'mbNear' = most recent same-slot non-match (see 'dropUntilLowerBound').
-- 'mbNear' = most recently processed same-slot non-match (see 'dropUntilLowerBound').

Comment on lines +1189 to +1190
sizedEntry :: SizedEntry blk -> Entry blk
sizedEntry (WithBlockSize _ e) = e

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.

I'd call this something else, like sizedEntryToEntry or entryFromSizedEntry, since it feels off to read, e.g., sizedEntry e where e is already a SizedEntry blk.


-- ChunkInfo where EBB epoch 0 maps to slot 0, sharing it with the first main block.
sharedSlotChunkInfo :: ChunkInfo
sharedSlotChunkInfo = UniformChunkSize $ ChunkSize True 10

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.

Maybe add a note that the 10 is entirely arbitrary, we remember later and don't get confused?

unit_dropUntilLowerBound_picksMainBlockAtSharedSlot =
case dropUntilLowerBound sharedSlotChunkInfo (SlotNo 0) mainHash sharedSlotEntries of
Right (WithBlockSize _ e : _) -> headerHash e @?= mainHash
other -> fail $ "expected suffix starting at main block, got " ++ show other

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.

"suffix" here feels odd, even if I know what you mean. Maybe just "result" or "iterable"?

unit_truncateAtUpperBound_picksEBBAtSharedSlot =
case truncateAtUpperBound sharedSlotChunkInfo (SlotNo 0) ebbHash sharedSlotEntries of
Right entries -> map (headerHash . sizedEntry) entries @?= [ebbHash]
other -> fail $ "expected prefix [EBB], got " ++ show other

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.

Analogous to the comment about "suffix", it feels a bit more natural to call "prefix" something like "result".

unit_truncateAtUpperBound_picksMainBlockAtSharedSlot =
case truncateAtUpperBound sharedSlotChunkInfo (SlotNo 0) mainHash sharedSlotEntries of
Right entries -> map (headerHash . sizedEntry) entries @?= [ebbHash, mainHash]
other -> fail $ "expected prefix [EBB, main], got " ++ show other

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.

Analogous to the comment about "suffix", it feels a bit more natural to call "prefix" something like "result".

Couple additional test cases + assorted tweaks to test cases
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