feat(state): add any-chain treestate-by-hash read requests#10820
Conversation
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
There was a problem hiding this comment.
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_treehelpers that iteratechain_iter()then fall back to the finalized DB, symmetric toread::any_block. - Add
ReadRequest::AnyChainSaplingTree/AnyChainOrchardTreevariants (reusing the existingReadResponse::SaplingTree/OrchardTree), wire up the dispatch andvariant_name, and document the root changelog. - Add
any_chain_treestate_finds_side_chain_treestest mirroringany_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. |
| - 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. |
There was a problem hiding this comment.
A breaking change entry needs to be added to the zebra-state/CHANGELOG.md.
|
/changelog |
|
Proposed changelog entries for this PR. Review and paste into the relevant ### 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.
|
jvff
left a comment
There was a problem hiding this comment.
Should we also open an issue to add the same request for Ironwood?
| - 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. |
There was a problem hiding this comment.
A breaking change entry needs to be added to the zebra-state/CHANGELOG.md.
| if best_hash == side_hash { | ||
| tracing::warn!("unable to create different block hashes, skipping side chain test"); | ||
| return Ok(()); | ||
| } |
There was a problem hiding this comment.
Minor: I think this should be an assertion.
| 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" | ||
| ); | ||
|
|
There was a problem hiding this comment.
Minor: Maybe also assert orchard?
| 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" | |
| ); | |
Motivation
ReadRequest::SaplingTree(HashOrHeight)andReadRequest::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 returnNoneeven though the block and its note commitment trees are still present in the non-finalized state.By contrast,
ReadRequest::AnyChainBlockalready 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_treefunctions that iteratechain_iter()(best chain first, then side chains by descending work) using the existing per-chainChain::sapling_tree/Chain::orchard_treeprimitives, then fall back to the finalized DB — mirroringread::any_block. The existing best-chainSaplingTree/OrchardTreerequests are unchanged.Tests
Added
any_chain_treestate_finds_side_chain_treestozebra-state/src/service/read/tests/vectors.rs, mirroring the existingany_chain_block_finds_side_chain_blockstest. It builds two competing non-finalized chains from a shared parent and asserts that:any_sapling_tree/any_orchard_treefind the side-chain treestate by hash, whilesapling_tree/orchard_tree(best-chain only) returnNone;cargo fmt --all -- --check,cargo clippy -p zebra-state --all-targets -- -D warnings, and the new test all pass.AI Disclosure
PR Checklist
🤖 Generated with Claude Code