Skip to content

feat(state): add any-chain treestate-by-hash read requests#10820

Open
arya2 wants to merge 1 commit into
mainfrom
fix-any-chain-treestate
Open

feat(state): add any-chain treestate-by-hash read requests#10820
arya2 wants to merge 1 commit into
mainfrom
fix-any-chain-treestate

Conversation

@arya2

@arya2 arya2 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Motivation

ReadRequest::SaplingTree(HashOrHeight) and ReadRequest::OrchardTree(HashOrHeight) resolve only against the best chain (latest_best_chain()), with a fallback to the finalized DB. When queried by a block hash that has since been reorged onto a still-retained non-finalized side chain, these return None even though the block and its note commitment trees are still present in the non-finalized state.

By contrast, ReadRequest::AnyChainBlock already provides reorg-immune block retrieval by iterating all non-finalized chains before falling back to the finalized DB. There was no equivalent for treestates, so a read-only consumer pinning a consistent view by hash could not make treestate reads reorg-immune.

Closes #10765

Solution

Add two any-chain treestate read requests, symmetric to AnyChainBlock:

  • ReadRequest::AnyChainSaplingTree(HashOrHeight) -> ReadResponse::SaplingTree(...)
  • ReadRequest::AnyChainOrchardTree(HashOrHeight) -> ReadResponse::OrchardTree(...)

These are backed by new read::any_sapling_tree / read::any_orchard_tree functions that iterate chain_iter() (best chain first, then side chains by descending work) using the existing per-chain Chain::sapling_tree / Chain::orchard_tree primitives, then fall back to the finalized DB — mirroring read::any_block. The existing best-chain SaplingTree / OrchardTree requests are unchanged.

Tests

Added any_chain_treestate_finds_side_chain_trees to zebra-state/src/service/read/tests/vectors.rs, mirroring the existing any_chain_block_finds_side_chain_blocks test. It builds two competing non-finalized chains from a shared parent and asserts that:

  • any_sapling_tree / any_orchard_tree find the side-chain treestate by hash, while sapling_tree / orchard_tree (best-chain only) return None;
  • both lookups find the best-chain treestate by hash.

cargo fmt --all -- --check, cargo clippy -p zebra-state --all-targets -- -D warnings, and the new test all pass.

AI Disclosure

  • AI tools were used: Claude Code for implementation and test boilerplate, under maintainer review.

PR Checklist

  • The PR title follows conventional commits format.
  • The solution is tested.
  • The documentation and changelogs are up to date.

🤖 Generated with Claude Code

Add `ReadRequest::AnyChainSaplingTree` and `ReadRequest::AnyChainOrchardTree`,
symmetric to `ReadRequest::AnyChainBlock`. They resolve a note commitment
treestate by hash against every non-finalized chain (best chain first, then
side chains) before falling back to the finalized state, instead of only the
best chain like the existing `SaplingTree` / `OrchardTree` requests.

This makes pinned treestate reads reorg-immune: a read-only consumer following
the tip can capture a hash and read its treestate without spuriously getting
`None` when a reorg moves that block onto a still-retained side chain. The
existing best-chain requests are unchanged.

Closes #10765
Copilot AI review requested due to automatic review settings June 26, 2026 06:05

Copilot AI 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.

Pull request overview

This PR adds reorg-immune, any-chain treestate-by-hash read requests to zebra-state, closing #10765. The existing ReadRequest::SaplingTree / OrchardTree resolve only against the best chain (with a finalized-DB fallback), so a treestate queried by a hash that has been reorged onto a still-retained non-finalized side chain returns None. The new requests mirror the existing AnyChainBlock pattern by iterating all non-finalized chains (best chain first) before falling back to the finalized DB, letting a read-only consumer pin a consistent, reorg-immune view by hash.

Changes:

  • Add read::any_sapling_tree / read::any_orchard_tree helpers that iterate chain_iter() then fall back to the finalized DB, symmetric to read::any_block.
  • Add ReadRequest::AnyChainSaplingTree / AnyChainOrchardTree variants (reusing the existing ReadResponse::SaplingTree/OrchardTree), wire up the dispatch and variant_name, and document the root changelog.
  • Add any_chain_treestate_finds_side_chain_trees test mirroring any_chain_block_finds_side_chain_blocks.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
zebra-state/src/service/read/tree.rs New any_sapling_tree / any_orchard_tree functions iterating all chains then finalized DB.
zebra-state/src/service/read.rs Re-exports the two new tree helpers.
zebra-state/src/service.rs Dispatches the new requests using latest_non_finalized_state().chain_iter().
zebra-state/src/request.rs Adds the two ReadRequest variants with docs and variant_name entries.
zebra-state/src/service/read/tests/vectors.rs Adds a side-chain treestate-by-hash test.
CHANGELOG.md Documents the new read requests.

Comment thread CHANGELOG.md
Comment on lines +30 to +34
- New `zebra-state` read requests `ReadRequest::AnyChainSaplingTree` and
`ReadRequest::AnyChainOrchardTree` that resolve a note commitment treestate by hash
against any non-finalized chain (not just the best chain) before falling back to the
finalized state, symmetric to `ReadRequest::AnyChainBlock`. This lets a read-only
consumer pin a consistent, reorg-immune view by reading treestates by hash.

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.

A breaking change entry needs to be added to the zebra-state/CHANGELOG.md.

@ZcashFoundation ZcashFoundation deleted a comment from github-actions Bot Jul 1, 2026
@ZcashFoundation ZcashFoundation deleted a comment from github-actions Bot Jul 1, 2026
@ZcashFoundation ZcashFoundation deleted a comment from github-actions Bot Jul 1, 2026
@gustavovalverde

Copy link
Copy Markdown
Member

/changelog

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Proposed changelog entries for this PR. Review and paste into the relevant
CHANGELOG.md [Unreleased] sections:

### Main `CHANGELOG.md`

```markdown
### Added

- `zebra-state` now exposes `ReadRequest::AnyChainSaplingTree` and
  `ReadRequest::AnyChainOrchardTree`, which look up note commitment treestates by
  block hash across every non-finalized chain (not just the best chain) before
  falling back to the finalized state, making those reads immune to reorgs.

zebra-state/CHANGELOG.md

### Added

- New `ReadRequest::AnyChainSaplingTree` and `ReadRequest::AnyChainOrchardTree`
  read requests that resolve a Sapling or Orchard note commitment treestate by
  block hash against any non-finalized chain, symmetric to
  `ReadRequest::AnyChainBlock`.

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

Should we also open an issue to add the same request for Ironwood?

Comment thread CHANGELOG.md
Comment on lines +30 to +34
- New `zebra-state` read requests `ReadRequest::AnyChainSaplingTree` and
`ReadRequest::AnyChainOrchardTree` that resolve a note commitment treestate by hash
against any non-finalized chain (not just the best chain) before falling back to the
finalized state, symmetric to `ReadRequest::AnyChainBlock`. This lets a read-only
consumer pin a consistent, reorg-immune view by reading treestates by hash.

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.

A breaking change entry needs to be added to the zebra-state/CHANGELOG.md.

Comment on lines +643 to +646
if best_hash == side_hash {
tracing::warn!("unable to create different block hashes, skipping side chain test");
return Ok(());
}

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.

Minor: I think this should be an assertion.

Suggested change
if best_hash == side_hash {
tracing::warn!("unable to create different block hashes, skipping side chain test");
return Ok(());
}
assert_ne!(best_hash, side_hash, "unable to create different block hashes");

.is_some(),
"sapling_tree should find best chain treestate by hash"
);

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.

Minor: Maybe also assert orchard?

Suggested change
assert!(
any_orchard_tree(
non_finalized_state.chain_iter(),
&finalized_state.db,
best_hash.into(),
)
.is_some(),
"any_orchard_tree should find best chain treestate by hash"
);
assert!(
orchard_tree(
non_finalized_state.best_chain(),
&finalized_state.db,
best_hash.into(),
)
.is_some(),
"orchard_tree should find best chain treestate by hash"
);

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.

zebra-state: any-chain treestate lookup by hash (SaplingTree/OrchardTree are best-chain-only)

6 participants