Skip to content

fix(state): make read_only_open_with_ephemeral_config_returns_error more robust - #11146

Merged
mergify[bot] merged 1 commit into
mainfrom
improve-read-only-ephemeral
Aug 1, 2026
Merged

fix(state): make read_only_open_with_ephemeral_config_returns_error more robust#11146
mergify[bot] merged 1 commit into
mainfrom
improve-read-only-ephemeral

Conversation

@conradoplg

@conradoplg conradoplg commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Closes #11147

Solution

The issue was actually in the code, we improve error checking which makes the test reliable and not dependent on a specific folder existing or not during testing

Tests

Specifications & References

Follow-up Work

AI Disclosure

  • No AI tools were used in this PR
  • AI tools were used: Claude, comments were cleaned up manually

PR Checklist

  • The PR title follows conventional commits format: type(scope): description
  • The PR follows the contribution guidelines.
  • This change was discussed in an issue or with the team beforehand.
  • The solution is tested.
  • The documentation and changelogs are up to date.

@conradoplg conradoplg added the C-exclude-from-changelog Category: The PR should be excluded from the changelog and release notes label Jul 29, 2026
@conradoplg conradoplg changed the title fix(state): make read_only_open_with_ephemeral_config_returns_error error checking more robust fix(state): make read_only_open_with_ephemeral_config_returns_error more robust Jul 29, 2026
@natalieesk

Copy link
Copy Markdown
Contributor

Closing as bug no longer exists

@natalieesk natalieesk closed this Jul 30, 2026
@natalieesk

Copy link
Copy Markdown
Contributor

Accidentally closed the wrong PR. Reopening.

@natalieesk natalieesk reopened this Jul 30, 2026
@v12-auditor

v12-auditor Bot commented Jul 30, 2026

Copy link
Copy Markdown

Note

Complete: Audit complete. V12 found four issues worth reviewing.

Open the full results here.

FindingSeverityDetails
F-107518 🟡 Medium
Read-only state open panics on unreadable or malformed version file

The read-only branch of ZebraDb::new() calls DiskDb::check_cache_dir_readable(&config.cache_dir) and then applies .expect("unable to read database format version file") to the result of database_format_version_on_disk(). The doc comment immediately above at lines 104-108 claims the readability check exists so that a missing or unreadable directory "returns a typed ReadOnlyCacheDirUnreadable error here instead of panicking on the version-file read", but the guard only performs fs::read_dir(cache_dir) on the top-level directory. The version file actually lives several levels deeper at cache_dir/<db_kind>/v<major>/<network>/version per Config::version_file_path and Config::db_path. In database_format_version_at_path, only ErrorKind::NotFound is swallowed into None; every other I/O error is propagated via Err(e)?, and a version file whose contents fail both the direct semver parse and the legacy "{major}.{content}" fallback returns Err(String). Any of these recoverable local conditions reaches the .expect and panics.

F-107520 🟡 Medium
Read-only open skips all database format validation

ZebraDb::new() forces debug_skip_format_upgrades = read_only || (cfg!(test) && debug_skip_format_upgrades), and ZebraDb::spawn_format_change() returns immediately when that flag is set. A single boolean therefore gates two distinct concerns: applying format upgrades, which writes and must be skipped on a read-only secondary, and running format validity checks, which are write-free and safe on a secondary. The skipped work includes format_validity_checks_detailed(), which the read-write path treats as fatal — it panics with "unexpected invalid database format: delete and re-sync the database". Its constituent checks only read (tip comparison, subtree pre-checks, genesis-root quick check, tree-key-type iteration, and per-upgrade validate implementations), so they could run safely read-only. Separately, the read-only path's only version-derived guard is format_change.is_newly_created(); DbFormatChange::open_database() can also return Upgrade when the disk version is older and Downgrade when it is newer, and neither is rejected or even warned about. An Upgrade state is reachable at a same-major path because try_reusing_previous_db_after_major_upgrade renames the old major directory to the new one and then deliberately writes the OLD major version into the NEW path's version file; if that upgrade is interrupted, the file stays stale indefinitely.

F-107521 🟡 Medium
Indexer-enabled read-only open serves empty spend indexes

Whether the on-disk database contains the spending-transaction indexes is recorded solely in the semver build metadata of the version file: state_database_format_version_in_code() sets build to "indexer" when the indexer feature is enabled and empty otherwise. Because semver cmp_precedence ignores build metadata, an on-disk 28.0.0 written by a non-indexer node compares Equal to an in-code 28.0.0+indexer, so open_database returns CheckOpenCurrent. On the read-write path that variant is exactly what triggers the backfill: the code writes the +indexer build metadata and runs track_tx_locs_by_spends::run(...) over the chain. On the read-only path this entire block is skipped because debug_skip_format_upgrades is forced true and spawn_format_change returns early, so the build metadata is never read, never compared, and the index is never built. The tx_loc_by_spent_out_loc column family is registered unconditionally, so the open succeeds with an empty CF and no open-time failure alerts the operator.

F-107522 🟡 Medium
Empty database directory causes misleading panic instead of typed error

The read-only guard in ZebraDb::new() rejects only the case where DbFormatChange::open_database reports NewlyCreated. Per database_format_version_at_path, NewlyCreated requires BOTH that the version file read returns NotFound AND that fs::metadata(db_path) returns NotFound. If the versioned database directory cache_dir/<db_kind>/v<major>/<net>/ exists but holds no RocksDB files — a partially-copied cache, an interrupted delete, or a primary that created the directory but has not yet initialized RocksDB — then fs::metadata succeeds and the function returns Some(Version::new(major, 0, 0)). is_newly_created() is then false, the guard is bypassed, and execution reaches DiskDb::new, where DB::open_cf_descriptors_as_secondary cannot create a database and fails. The failure is handled by a panic! whose message reads "Database likely already open ... Hint: Check if another zebrad process is running", which misdiagnoses the actual condition.

And four more auto-invalidated findings.

Analyzed one file, diff 21b652b...2c187ad.

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

I went through the four v12 findings and verified each against the code. All four are mechanically real, but all four are pre-existing conditions of the read-only open path on main — none are introduced by this PR — so I don't think any of them should block it. Summary:

  • F-107518 (panic on unreadable/malformed version file): real. The .expect at zebra_db.rs:113 panics when the version file exists but is permission-denied or unparseable; check_cache_dir_readable only guards the top-level cache dir, so the doc comment above it overclaims. Note the read-write path has the identical .expect, so read-only is no worse — but a typed error would be better for a secondary reading a primary's cache.
  • F-107520 (read-only skips all format validation): real behaviorally, partly by design. Skipping upgrades read-only is required; silently accepting Upgrade/Downgrade disk versions is a genuine gap (the stale-version-file scenario via an interrupted try_reusing_previous_db_after_major_upgrade is real — it deliberately writes the old major into the new path's version file). Caveat on the suggested remedy: running the fatal validity checks on a secondary that lags a mid-upgrade primary could fail spuriously and panic, so "the checks are safe read-only" isn't clear-cut. A warn/error on Upgrade/Downgrade in read-only mode seems like the right-sized fix.
  • F-107521 (indexer read-only serves empty spend indexes): real, and the most consequential — it produces silently wrong results (None) rather than a crash, because the +indexer marker lives only in semver build metadata, which cmp_precedence ignores, and the backfill only runs from the format-change task that read-only mode skips. Filed as #11155.
  • F-107522 (empty versioned dir → misleading "Database likely already open" panic): real. An empty cache_dir/state/vN/<net>/ yields Some(N.0.0), bypasses the newly-created guard, and the secondary open failure panics with a misleading hint. Error-message quality issue; deserves a typed error.

This PR's change itself is correct: it fixes the real root cause of the test flake (error-precedence depended on whether the default cache dir existed on the machine) and matches the authoritative check in DiskDb::new(). It also avoids the temp-dir leak from calling database_format_version_on_disk() with ephemeral = true. LGTM; the findings above are follow-up material for the read-only open feature, not this fix.

@mergify mergify Bot added the queued label Jul 30, 2026
@mergify

mergify Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

This pull request spent 36 minutes 27 seconds in the queue, including 35 minutes 54 seconds running CI.

Waiting for any of
  • check-neutral = unit-tests
  • check-skipped = unit-tests
  • check-success = unit-tests
All conditions

Reason

The merge conditions cannot be satisfied due to failing checks

  • @github-actions/lint
  • test-crates
  • unit-tests

Failing checks:

Hint

You may have to fix your CI before adding the pull request to the queue again.
If you update this pull request, to fix the CI, it will automatically be requeued once the queue conditions match again.
If you think this was a flaky issue instead, you can requeue the pull request, without updating it, by posting a @mergifyio queue comment.

Requeued — the merge queue status continues in this comment ↓.

@mergify

mergify Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • Entered queue2026-07-31 14:42 UTC · Rule: batched · triggered by rule move to any queue if GitHub Rulesets are satisfied
  • Checks failed · on draft merge queue: checking main (25d0c1b) and #11146 together #11160
  • 🚫 Left the queue2026-07-31 14:44 UTC · at 2c187ad6ab7992dd34d4204f991ccd156e950ad9

This pull request spent 2 minutes 9 seconds in the queue, with no time running CI.

Waiting for
  • any of: [🛡 GitHub repository ruleset rule PR Requirements]
    • check-neutral = @github-actions/lint
    • check-skipped = @github-actions/lint
    • check-success = @github-actions/lint
  • any of: [🛡 GitHub repository ruleset rule PR Requirements]
    • check-neutral = test-crates
    • check-skipped = test-crates
    • check-success = test-crates
  • any of: [🛡 GitHub repository ruleset rule PR Requirements]
    • check-neutral = unit-tests
    • check-skipped = unit-tests
    • check-success = unit-tests
All conditions

Reason

The merge conditions cannot be satisfied due to failing checks

Hint

You may have to fix your CI before adding the pull request to the queue again.
If you update this pull request, to fix the CI, it will automatically be requeued once the queue conditions match again.
If you think this was a flaky issue instead, you can requeue the pull request, without updating it, by posting a @mergifyio queue comment.

Requeued — the merge queue status continues in this comment ↓.

@mergify

mergify Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

queue

☑️ This pull request is already queued

@mergify

mergify Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

This pull request spent 2 minutes 29 seconds in the queue, with no time running CI.

Waiting for
  • any of: [🛡 GitHub repository ruleset rule PR Requirements]
    • check-neutral = @github-actions/lint
    • check-skipped = @github-actions/lint
    • check-success = @github-actions/lint
  • any of: [🛡 GitHub repository ruleset rule PR Requirements]
    • check-neutral = test-crates
    • check-skipped = test-crates
    • check-success = test-crates
  • any of: [🛡 GitHub repository ruleset rule PR Requirements]
    • check-neutral = unit-tests
    • check-skipped = unit-tests
    • check-success = unit-tests
All conditions

Reason

The merge conditions cannot be satisfied due to failing checks

Hint

You may have to fix your CI before adding the pull request to the queue again.
If you update this pull request, to fix the CI, it will automatically be requeued once the queue conditions match again.
If you think this was a flaky issue instead, you can requeue the pull request, without updating it, by posting a @mergifyio queue comment.

Requeued — the merge queue status continues in this comment ↓.

@mergify

mergify Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • Entered queue2026-08-01 00:08 UTC · Rule: batched · triggered by @alchemydc with the merge queue checkbox
  • Checks skipped · PR is already up-to-date
  • Merged2026-08-01 00:09 UTC · at 2bbefc2a140d38990a922ebabdfaf24b9ef7fe12 · merge

This pull request spent 51 seconds in the queue, including 11 seconds running CI.

Required conditions to merge

@mergify mergify Bot added the queued label Aug 1, 2026
@mergify
mergify Bot merged commit 67eac5c into main Aug 1, 2026
195 of 197 checks passed
@mergify
mergify Bot deleted the improve-read-only-ephemeral branch August 1, 2026 00:09
@mergify mergify Bot removed the queued label Aug 1, 2026
jvff added a commit that referenced this pull request Aug 6, 2026
Link the read-only ephemeral configuration fix to PR #11146 so crate
consumers can find its implementation and tests.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the read-only ephemeral configuration fix to PR #11146 so crate
consumers can find its implementation and tests.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the read-only ephemeral configuration fix to PR #11146 so crate
consumers can find its implementation and tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-exclude-from-changelog Category: The PR should be excluded from the changelog and release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: read_only_open_with_ephemeral_config_returns_error sometimes fail

3 participants