Skip to content

fix(file_store): stop iterating when a decoded entry consumes no bytes - #2333

Open
Brijesh-Thakkar wants to merge 2 commits into
bitcoindevkit:masterfrom
Brijesh-Thakkar:fix/file-store-zero-width-load-hang
Open

Brijesh-Thakkar wants to merge 2 commits into
bitcoindevkit:masterfrom
Brijesh-Thakkar:fix/file-store-zero-width-load-hang

Conversation

@Brijesh-Thakkar

Copy link
Copy Markdown

Description

Fixes #2307.

Store::load never returns when the changeset type encodes to zero bytes (e.g. Store::<()>).

EntryIter::next only detects end-of-file through an UnexpectedEof error raised when zero bytes are consumed. A zero-width type decodes successfully without reading anything, so that error never fires. The file offset never advances, and load → dump iterates forever.

The fix compares stream_position() before and after a successful decode in the Ok arm. If the offset did not advance, the iterator is finished and a StoreError::Bincode error is returned. Normal changesets always consume at least one byte, so their behavior is unchanged. There are no public API, dependency or on-disk format changes.

Notes to the reviewers

  • The trailing byte in the issue's repro is not required. A file containing only the magic bytes hangs the same way for Store::<()>. Both cases are covered by tests.
  • After this change Store::<()> returns an error in both cases instead of hanging, so it can no longer be loaded at all. I think this is acceptable because append never writes empty changesets (is_empty is checked first) and no real BDK changeset is zero-width. Returning Ok for a magic-only file would need an EOF peek like the one in Replace bincode by postcard #2258, which is a larger diff.
  • I reused the existing StoreError::Bincode variant with ErrorKind::Custom to avoid a breaking change, since StoreError is not #[non_exhaustive]. Io(InvalidData) would also work if you prefer it.
  • This is independent of Replace bincode by postcard #2258 (bincode → postcard), which would fix the hang through length-prefixed framing. The two overlap in the Ok arm of next(). I put the tests in a new file (crates/file_store/tests/test_zero_width.rs) to avoid conflicts with the store.rs tests. If Replace bincode by postcard #2258 lands first, this code change can be dropped and the tests kept as a regression guard. I'm happy to rebase or close this in favor of Replace bincode by postcard #2258, whichever you prefer.
  • The tests run load on a detached thread with a 5-second timeout. Both time out without the fix. On failure, the thread keeps spinning until the test process exits.
  • This was written with Claude Code assistance and reviewed by me.

Changelog notice

Fixed

  • Store::load no longer loops forever when the changeset type encodes to zero bytes (e.g. ()). It now returns StoreError::Bincode when a decoded entry consumes no bytes.

Checklists

All Submissions:

Bugfixes:

  • This pull request breaks the existing API
  • I've added tests to reproduce the issue which are now passing
  • I'm linking the issue being fixed by this PR

EntryIter::next only detects end-of-file through an UnexpectedEof error
raised when zero bytes are consumed. A changeset type whose bincode
encoding is zero bytes (e.g. `()`) never raises that error: every decode
succeeds without advancing the file offset, so Store::load never returns.

Compare the stream position before and after a successful decode. If it
did not advance, finish the iterator and return a StoreError::Bincode
error. Normal changesets always consume at least one byte, so their
behavior is unchanged. There are no public API changes.

Add regression tests that time out without this change.

Fixes bitcoindevkit#2307
Copilot AI balanced review requested due to automatic review settings September 30, 2026 08:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

NACK 717f436

bincode has been deprecated. Any solution for the file store issues should also carry the migration from bincode to postcard.

`bincode` is deprecated. Replace it with `postcard` and frame each entry
as a `u64` varint length prefix followed by the `postcard`-encoded
changeset. The length prefix makes the end of an entry explicit instead
of relying on the decoder hitting `UnexpectedEof`.

This also fixes `Store::load` never terminating when a changeset type
encodes to zero bytes (e.g. `()`): every entry now occupies at least the
length prefix, so the file offset always advances. A clean end-of-file
is detected by peeking the buffer before reading an entry. The
`Ok`-arm progress check from the previous commit is superseded by this
and is removed.

Add regression tests for zero-width changesets.

The entry_iter, lib and Cargo.toml changes are taken from bitcoindevkit#2258.

BREAKING CHANGE: the on-disk format changes, so existing store files
written with `bincode` cannot be loaded. `StoreError::Bincode` is
replaced by `StoreError::Decode(postcard::Error)`.

Fixes bitcoindevkit#2307

Co-authored-by: nymius <155548262+nymius@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

file_store: Store::load never terminates when a changeset entry decodes to zero bytes

3 participants