Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7ec528f
test: add red driver for #1096 — best_chaintip must derive from NFS…
zancas May 26, 2026
e5e0665
zaino-state: add ProvisionalBlock and relative-work type for #1096
zancas May 27, 2026
a5a2b29
ignore red test
zancas May 27, 2026
d97d617
zaino-state: store ProvisionalBlock in the NFS (#1096 wiring)
zancas May 27, 2026
366350f
rm dead code
zancas May 27, 2026
64b3209
zaino-state: remove dead code left by the ProvisionalBlock wiring
zancas May 27, 2026
cd07828
Introduce the Provisional/Resolved availability the snapshot-collap…
zancas May 27, 2026
2b992f9
Eliminate the StillSyncingFinalizedState path. The non-finalized st…
zancas May 27, 2026
469e70c
chain_index: make the non-finalized state lead, anchored at the fin…
zancas May 27, 2026
cc6d49f
chain_index tests: add Resolved full-coverage test; cache proptest …
zancas May 27, 2026
b5ecb54
chain_index tests: add Resolved full-coverage test; cache proptest …
zancas May 27, 2026
e871653
zaino-state backends: serve reads from the always-present NFS, not on…
zancas May 27, 2026
6721178
make test should_panic
zancas May 27, 2026
736b7ef
Merge branch 'dev' into define_provisional_block_type_for_NFS_type_sa…
zancas May 27, 2026
10aa7bf
Merge branch 'dev' into wire_in_provisional_blocks
zancas May 27, 2026
927ffa7
Merge branch 'dev' into define_provisional_block_type_for_NFS_type_sa…
AloeareV Jun 10, 2026
247f0c4
Merge branch 'define_provisional_block_type_for_NFS_type_safety' into…
AloeareV Jun 10, 2026
c3a2f95
publicize new types now used in public API
AloeareV Jun 10, 2026
fad9f7b
publicize some fields of ProvisionalBlock, update changelog
AloeareV Jun 10, 2026
79002dd
Merge branch 'wire_in_provisional_blocks' into instantiate_NFS_at_launch
AloeareV Jun 10, 2026
cc2c9e3
remove unneeded attributes
AloeareV Jun 11, 2026
abf29d1
remove old attribute
AloeareV Jun 11, 2026
55fd703
add cfg test
AloeareV Jun 11, 2026
fffbff2
calculate provisional chainwork on demand
AloeareV Jun 11, 2026
76661fe
initialize nfs from source instead of indexedblock, not yet error han…
AloeareV Jun 11, 2026
7335b4f
remove now-unused code
AloeareV Jun 11, 2026
b171a4c
some error handling plus start on reify fn
AloeareV Jun 12, 2026
e286afa
seam_block error when missing
AloeareV Jun 16, 2026
6bac83e
impl Block for IndexedBlock, more reify fn work
AloeareV Jun 16, 2026
d1038c9
finish (non_error_handled) reify fn
AloeareV Jun 16, 2026
c976148
provisional_cumulative_work now aggregates each block's work at calltime
AloeareV Jun 19, 2026
27cc7c1
wip removal of Provisional block in favor of Provisional chainwork en…
AloeareV Jun 19, 2026
a405f10
Merge remote-tracking branch 'origin/dev' into reify_NFS_when_FS_synced
AloeareV Jun 19, 2026
84b2c0c
Merge remote-tracking branch 'origin/dev' into reify_NFS_when_FS_synced
AloeareV Jun 19, 2026
3a700d1
Merge remote-tracking branch 'origin/reify_NFS_when_FS_synced' into r…
AloeareV Jun 24, 2026
b045f89
Merge remote-tracking branch 'origin/dev' into reify_NFS_when_FS_synced
AloeareV Jun 24, 2026
626237b
Merge remote-tracking branch 'origin/dev' into reify_NFS_when_FS_synced
AloeareV Jun 24, 2026
8683032
Merge branch 'dev' into reify_NFS_when_FS_synced
AloeareV Jul 1, 2026
dc84370
wait for FS to catch up in test
AloeareV Jul 1, 2026
7f82670
fix missing roots in proptests
AloeareV Jul 1, 2026
ccb3c13
fix race condition test
AloeareV Jul 1, 2026
8684e98
update changelog
AloeareV Jul 1, 2026
2937e08
Merge remote-tracking branch 'origin/dev' into reify_NFS_when_FS_synced
AloeareV Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion live-tests/e2e/tests/test_vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ async fn create_200_block_regtest_chain_vectors() {
// Update parent block
parent_block_sapling_tree_size = chain_block.commitment_tree_data().sizes().sapling();
parent_block_orchard_tree_size = chain_block.commitment_tree_data().sizes().orchard();
parent_chain_work = Some(*chain_block.chainwork());
parent_chain_work = *chain_block.chainwork();

data.push((height, zebra_block, block_roots, block_treestate));
}
Expand Down
27 changes: 13 additions & 14 deletions live-tests/zaino-testutils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,34 +223,33 @@ pub trait PollableTip: Status + Sync {

impl PollableTip for FetchServiceSubscriber {
async fn tip_height(&self) -> u64 {
// A not-yet-ready backend reports tip 0 (poll keeps waiting) rather
// than panicking.
self.get_latest_block()
.await
.expect("PollableTip: FetchServiceSubscriber::get_latest_block failed")
.height
.map(|block| block.height)
.unwrap_or(0)
}
}

impl PollableTip for StateServiceSubscriber {
async fn tip_height(&self) -> u64 {
self.get_latest_block()
.await
.expect("PollableTip: StateServiceSubscriber::get_latest_block failed")
.height
.map(|block| block.height)
.unwrap_or(0)
}
}

impl<Source: BlockchainSource> PollableTip for NodeBackedChainIndexSubscriber<Source> {
async fn tip_height(&self) -> u64 {
let snapshot = self
.snapshot_nonfinalized_state()
.await
.expect("PollableTip: chain-index snapshot_nonfinalized_state failed");
u64::from(u32::from(
self.best_chaintip(&snapshot)
.await
.expect("PollableTip: chain-index best_chaintip failed")
.height,
))
let Ok(snapshot) = self.snapshot_nonfinalized_state().await else {
return 0;
};
match self.best_chaintip(&snapshot).await {
Ok(tip) => u64::from(u32::from(tip.height)),
Err(_) => 0,
}
}
}

Expand Down
11 changes: 11 additions & 0 deletions packages/zaino-state/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this library adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased not landing in 0.4.0]
### Added

### Changed
- `chain_index::types::BlockContext`'s chainwork field is now optional
- `chain_index::ChainIndexSnapshot` has been completely overhauled, is now
a struct instead of an enum, with completely private internals. Callers
must now interface with it only by passing it to a ChainIndex implementor,
as intended.

## [Unreleased]

### Added
Expand Down Expand Up @@ -47,6 +57,7 @@ and this library adheres to Rust's notion of
can no longer inflate each other's peak memory.
### Deprecated
### Removed
- `NonFinalizedSnapshot::max_serviceable_height`
### Fixed
- The finalised-state txout-set accumulator rebuild at chain tip no longer
OOM-crashes on memory-constrained hosts. It auto-shards its in-memory spent set
Expand Down
95 changes: 16 additions & 79 deletions packages/zaino-state/src/backends/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,7 @@ use crate::{
utils::{get_build_info, ServiceMetadata},
BackendType,
};
use crate::{
chain_index::{non_finalised_state::ChainIndexSnapshot, NonFinalizedSnapshot},
ChainIndex, NodeBackedChainIndex, NodeBackedChainIndexSubscriber,
};
use crate::{ChainIndex, NodeBackedChainIndex, NodeBackedChainIndexSubscriber};

/// Chain fetch service backed by Zcashd's JsonRPC engine.
///
Expand Down Expand Up @@ -499,7 +496,7 @@ impl ZcashIndexer for FetchServiceSubscriber {

async fn get_chain_tips(&self) -> Result<GetChainTipsResponse, Self::Error> {
let snapshot = self.indexer.snapshot_nonfinalized_state().await?;
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
let Some(non_finalized_snapshot) = snapshot.resolved_nfs_snapshot() else {
return Ok(self.fetcher.get_chain_tips().await?);
};

Expand Down Expand Up @@ -758,7 +755,9 @@ impl ZcashIndexer for FetchServiceSubscriber {
let (height, confirmations, block_hash, in_best_chain) = match best_chain_location {
Some(types::BestChainLocation::Block(block_hash, height)) => {
let confirmations = snapshot
.max_serviceable_height()
.get_nfs_snapshot()
.best_tip
.height
.0
.saturating_sub(height.0)
.saturating_add(1);
Expand Down Expand Up @@ -815,7 +814,9 @@ impl ZcashIndexer for FetchServiceSubscriber {
self.indexer
.snapshot_nonfinalized_state()
.await?
.max_serviceable_height()
.get_nfs_snapshot()
.best_tip
.height
.0,
))
}
Expand Down Expand Up @@ -896,18 +897,8 @@ impl ZcashIndexer for FetchServiceSubscriber {
impl LightWalletIndexer for FetchServiceSubscriber {
/// Return the height of the tip of the best chain
async fn get_latest_block(&self) -> Result<BlockId, Self::Error> {
match self.indexer.snapshot_nonfinalized_state().await? {
ChainIndexSnapshot::NonFinalizedStateExists {
non_finalized_snapshot,
} => Ok(non_finalized_snapshot.best_tip.to_wire()),
ChainIndexSnapshot::StillSyncingFinalizedState { .. } => {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of reporting
// the genesis block
Err(FetchServiceError::UnavailableNotSyncedEnough)
}
}
// dbg!(&tip);
let snapshot = self.indexer.snapshot_nonfinalized_state().await?;
Ok(snapshot.get_nfs_snapshot().best_tip.to_wire())
}

/// Return the compact block corresponding to the given block identifier
Expand Down Expand Up @@ -938,12 +929,7 @@ impl LightWalletIndexer for FetchServiceSubscriber {
}
};

let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of
// acting as if we are only synced to the genesis block
return Err(FetchServiceError::UnavailableNotSyncedEnough);
};
let non_finalized_snapshot = snapshot.get_nfs_snapshot();

match self
.indexer
Expand Down Expand Up @@ -1018,12 +1004,7 @@ impl LightWalletIndexer for FetchServiceSubscriber {
}
}
};
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of
// acting as if we are only synced to the genesis block
return Err(FetchServiceError::UnavailableNotSyncedEnough);
};
let non_finalized_snapshot = snapshot.get_nfs_snapshot();
match self
.indexer
.get_compact_block(
Expand Down Expand Up @@ -1119,20 +1100,7 @@ impl LightWalletIndexer for FetchServiceSubscriber {
let timeout_result = timeout(
time::Duration::from_secs((service_timeout * 4) as u64),
async {
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of
// acting as if we are only synced to the genesis block
if let Err(e) = channel_tx
.send(Err(tonic::Status::failed_precondition(
"zaino not yet synced".to_string(),
)))
.await
{
warn!(%e, "GetBlockRange channel closed unexpectedly");
};
return;
};
let non_finalized_snapshot = snapshot.get_nfs_snapshot();
// Use the snapshot tip directly, as this function doesn't support passthrough
let chain_height = non_finalized_snapshot.best_tip.height.0;

Expand Down Expand Up @@ -1252,20 +1220,7 @@ impl LightWalletIndexer for FetchServiceSubscriber {
let timeout_result = timeout(
time::Duration::from_secs((service_timeout * 4) as u64),
async {
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of
// acting as if we are only synced to the genesis block
if let Err(e) = channel_tx
.send(Err(tonic::Status::failed_precondition(
"zaino not yet synced".to_string(),
)))
.await
{
warn!(%e, "GetBlockRangeNullifiers channel closed unexpectedly");
};
return;
};
let non_finalized_snapshot = snapshot.get_nfs_snapshot();

// Use the snapshot tip directly, as this function doesn't support passthrough
let chain_height = non_finalized_snapshot.best_tip.height.0;
Expand Down Expand Up @@ -1389,12 +1344,7 @@ impl LightWalletIndexer for FetchServiceSubscriber {
// Zebra returns None for mempool transactions, convert to `Mempool Height`.
None => {
let snapshot = self.indexer.snapshot_nonfinalized_state().await?;
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of
// acting as if we are only synced to the genesis block
return Err(FetchServiceError::UnavailableNotSyncedEnough);
};
let non_finalized_snapshot = snapshot.get_nfs_snapshot();
non_finalized_snapshot.best_tip.height.0 as u64
}
};
Expand Down Expand Up @@ -1745,20 +1695,7 @@ impl LightWalletIndexer for FetchServiceSubscriber {
let timeout = timeout(
time::Duration::from_secs((service_timeout * 6) as u64),
async {
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of
// acting as if we are only synced to the genesis block
if let Err(e) = channel_tx
.send(Err(tonic::Status::failed_precondition(
"zaino not yet synced".to_string(),
)))
.await
{
warn!(%e, "GetMempoolStream channel closed unexpectedly");
};
return;
};
let non_finalized_snapshot = snapshot.get_nfs_snapshot();
let mempool_height = non_finalized_snapshot.best_tip.height.0;
match indexer.get_mempool_stream(None) {
Some(mut mempool_stream) => {
Expand Down
63 changes: 18 additions & 45 deletions packages/zaino-state/src/backends/state.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Zcash chain fetch and tx submission service backed by Zebras [`ReadStateService`].

use crate::{chain_index::types::BestChainLocation, TransactionHash};
#[allow(deprecated)]
use crate::{
chain_index::{
Expand All @@ -20,10 +21,6 @@ use crate::{
utils::{get_build_info, ServiceMetadata},
BackendType, NodeBackedChainIndex, NodeBackedChainIndexSubscriber, State,
};
use crate::{
chain_index::{types::BestChainLocation, NonFinalizedSnapshot},
TransactionHash,
};
use tokio_stream::StreamExt as _;
use zaino_fetch::{
chain::{transaction::FullTransaction, utils::ParseFromSlice},
Expand Down Expand Up @@ -578,9 +575,8 @@ impl StateServiceSubscriber {
let timeout_result = timeout(
time::Duration::from_secs((service_timeout * 4) as u64),
async {
// This method does not support passthrough. Just return.
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {return};
let chain_height = non_finalized_snapshot.best_tip.height.0;
let nfs_snapshot = snapshot.get_nfs_snapshot();
let chain_height = nfs_snapshot.best_tip.height.0;

match state_service_clone
.indexer
Expand Down Expand Up @@ -688,7 +684,7 @@ impl StateServiceSubscriber {
height: u32,
) -> Result<CompactBlock, StateServiceError> {
let snapshot = self.indexer.snapshot_nonfinalized_state().await?;
let chain_height = snapshot.max_serviceable_height().0;
let chain_height = snapshot.get_nfs_snapshot().best_tip.height.0;
Err(if height >= chain_height {
StateServiceError::TonicStatusError(tonic::Status::out_of_range(format!(
"Error: Height out of range [{height}]. Height requested \
Expand Down Expand Up @@ -1567,19 +1563,14 @@ impl ZcashIndexer for StateServiceSubscriber {
/// tags: blockchain
async fn get_block_count(&self) -> Result<Height, Self::Error> {
let snapshot = self.indexer.snapshot_nonfinalized_state().await?;
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of
// acting as if we are only synced to the genesis block
return Err(StateServiceError::UnavailableNotSyncedEnough);
};
let h = non_finalized_snapshot.best_tip.height;
let nfs_snapshot = snapshot.get_nfs_snapshot();
let h = nfs_snapshot.best_tip.height;
Ok(h.into())
}

async fn get_chain_tips(&self) -> Result<GetChainTipsResponse, Self::Error> {
let snapshot = self.indexer.snapshot_nonfinalized_state().await?;
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
let Some(non_finalized_snapshot) = snapshot.resolved_nfs_snapshot() else {
return Ok(self.rpc_client.get_chain_tips().await?);
};

Expand Down Expand Up @@ -1790,7 +1781,9 @@ impl ZcashIndexer for StateServiceSubscriber {
let (height, confirmations, block_hash, in_best_chain) = match best_chain_location {
Some(BestChainLocation::Block(block_hash, height)) => {
let confirmations = snapshot
.max_serviceable_height()
.get_nfs_snapshot()
.best_tip
.height
.0
.saturating_sub(height.0)
.saturating_add(1);
Expand Down Expand Up @@ -1905,13 +1898,8 @@ impl LightWalletIndexer for StateServiceSubscriber {
/// Return the height of the tip of the best chain
async fn get_latest_block(&self) -> Result<BlockId, Self::Error> {
let snapshot = self.indexer.snapshot_nonfinalized_state().await?;
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of
// acting as if we are only synced to the genesis block
return Err(StateServiceError::UnavailableNotSyncedEnough);
};
Ok(non_finalized_snapshot.best_tip.to_wire())
let nfs_snapshot = snapshot.get_nfs_snapshot();
Ok(nfs_snapshot.best_tip.to_wire())
}

/// Return the compact block corresponding to the given block identifier
Expand Down Expand Up @@ -1946,13 +1934,8 @@ impl LightWalletIndexer for StateServiceSubscriber {
{
Ok(Some(block)) => Ok(block),
Ok(None) => {
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of
// acting as if we are only synced to the genesis block
return Err(StateServiceError::UnavailableNotSyncedEnough);
};
let chain_height = non_finalized_snapshot.best_tip.height.0;
let nfs_snapshot = snapshot.get_nfs_snapshot();
let chain_height = nfs_snapshot.best_tip.height.0;
match hash_or_height {
HashOrHeight::Height(Height(height)) if height >= chain_height => Err(
StateServiceError::TonicStatusError(tonic::Status::out_of_range(format!(
Expand All @@ -1966,13 +1949,8 @@ impl LightWalletIndexer for StateServiceSubscriber {
}
}
Err(e) => {
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of
// acting as if we are only synced to the genesis block
return Err(StateServiceError::UnavailableNotSyncedEnough);
};
let chain_height = non_finalized_snapshot.best_tip.height.0;
let nfs_snapshot = snapshot.get_nfs_snapshot();
let chain_height = nfs_snapshot.best_tip.height.0;
match hash_or_height {
HashOrHeight::Height(Height(height)) if height >= chain_height => Err(
StateServiceError::TonicStatusError(tonic::Status::out_of_range(format!(
Expand Down Expand Up @@ -2400,13 +2378,8 @@ impl LightWalletIndexer for StateServiceSubscriber {
let (channel_tx, channel_rx) =
mpsc::channel(self.config.common.service.channel_size as usize);
let snapshot = self.indexer.snapshot_nonfinalized_state().await?;
let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else {
// TODO: This probably shouldn't be an error.
// this is an improvement over previous behaviour of
// acting as if we are only synced to the genesis block
return Err(StateServiceError::UnavailableNotSyncedEnough);
};
let mempool_height = non_finalized_snapshot.best_tip.height.0;
let nfs_snapshot = snapshot.get_nfs_snapshot();
let mempool_height = nfs_snapshot.best_tip.height.0;
tokio::spawn(async move {
let timeout = timeout(
time::Duration::from_secs((service_timeout * 6) as u64),
Expand Down
Loading
Loading