From eeb30559c9473a254dc4dd82e9d42ec27b9bfcad Mon Sep 17 00:00:00 2001 From: YaroShkvorets Date: Thu, 8 Oct 2026 19:50:55 -0400 Subject: [PATCH] Start a live stream at the next block when the start block number is missing Fixes #82 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + forkable/forkable.go | 2 +- forkable/forkable_test.go | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d4f6d3..39b2161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A live stream starting at a block number with no block (a missed or skipped slot) now starts at the next block instead of waiting for the merged bundle containing it. - `hub.ForkableHub` no longer breaks when its one-block bootstrap races with the merger deleting the files of a bundle it just merged. Concretely: bootstrap retries a few times when the listing comes back with a gap or a listed file is gone by the time it is opened (both expected during that race), a failed bootstrap no longer leaves the forkable holding a bogus LIB it then can't reconcile with live blocks (the hub now only swaps in the forkable it built once bootstrap actually succeeds, falling back to a fresh one otherwise), and `cannot link block after reconnection` is only fatal once the hub has actually been ready — before that, an unlinkable block is handled like any other, already subject to `WithMaxConsecutiveUnlinkableBlocks`. Previously a consumer starting or restarting during that race could see the hub die or never become ready. - The hub no longer walks the one-block store for a block it already has below LIB. With redundant live sources, a source re-sending a final block that the hub still keeps below LIB (the lowest one, whose parent was already pruned) triggered a store lookup and a `block not linkable after one-block lookup` warning, even though the forkable drops blocks below LIB anyway. The hub now compares against LIB instead of the lowest kept block. `Forkable.LIBNum` is added for this. - `hub.WithMaxConsecutiveUnlinkableBlocks` now counts blocks rather than messages: an intermediate flash block — `PartialIndex != 0` without `LastPartial` — no longer advances the counter. Every partial of a block fails the same link check, so on a chain delivering four per block the hub gave up after a quarter of the blocks the limit names. A plain block and a block's final partial still count, and any linkable block still resets the count. diff --git a/forkable/forkable.go b/forkable/forkable.go index 7f6c80b..fdaa770 100644 --- a/forkable/forkable.go +++ b/forkable/forkable.go @@ -203,7 +203,7 @@ func (p *Forkable) blocksFromNum(num uint64) ([]*bstream.PreprocessedBlock, erro var seenBlock bool for i := range seg { ref := seg[i].AsRef() - if !seenBlock && ref.Num() == num { + if !seenBlock && ref.Num() >= num { seenBlock = true } diff --git a/forkable/forkable_test.go b/forkable/forkable_test.go index 3a73a36..ea8d5db 100644 --- a/forkable/forkable_test.go +++ b/forkable/forkable_test.go @@ -1800,6 +1800,27 @@ func TestForkable_BlocksFromIrreversibleNum(t *testing.T) { }, }, }, + { + name: "missing block number starts at the next block", + forkdbBlocks: []*pbbstream.Block{ + bstream.TestBlockWithLIBNum("00000005", "00000004", 2), + bstream.TestBlockWithLIBNum("00000008", "00000005", 4), + bstream.TestBlockWithLIBNum("00000009", "00000008", 5), + }, + requestBlock: 6, + expectBlocks: []expectedBlock{ + { + bstream.TestBlockWithLIBNum("00000008", "00000005", 4), + bstream.StepNew, + 5, + }, + { + bstream.TestBlockWithLIBNum("00000009", "00000008", 5), + bstream.StepNew, + 5, + }, + }, + }, { name: "no source", forkdbBlocks: []*pbbstream.Block{