Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org).

### Performance

- Skip the transparent address index (balances, address→utxo, address→tx) on a
pruned, checkpoint-syncing node. The index is RPC-only state, not consensus, so
the minimal fast-validator configuration no longer does the per-block
address-balance reads or the index writes (like pruned mode already skips
raw-transaction storage). Address-lookup RPCs (`getaddressbalance`,
`getaddressutxos`, `getaddresstxids`) return an explicit "index disabled"
error in this mode instead of wrong (empty) results. Archive nodes, and pruned
nodes with checkpoint sync disabled (full semantic verification), are unchanged.
- Compute the v5 ZIP-244 txid and authorizing-data digest natively. Both
previously routed through `Transaction::to_librustzcash`, which re-serializes
and reparses the whole transaction — decompressing every Jubjub and Pallas
Expand Down
49 changes: 49 additions & 0 deletions zebra-state/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,22 @@ impl Config {
}
}

/// Whether to omit archive/indexer-only data that is not required for consensus.
///
/// These indexes are RPC-only state, not consensus. They are skipped only for
/// the minimal fast-validator configuration: a node that is both
/// [`StorageMode::Pruned`] **and** checkpoint-syncing
/// ([`checkpoint_sync`](Config::checkpoint_sync)). This drops historical
/// transparent address-index writes and transparent finalized-spender lookups,
/// just as pruned mode drops raw-transaction storage.
///
/// An archive node keeps these indexes; so does a node with checkpoint sync
/// disabled (full semantic verification), even when pruned. RPCs backed by
/// skipped indexes return an error rather than wrong (empty) results.
pub fn skip_archive_indexes(&self) -> bool {
matches!(self.storage_mode, StorageMode::Pruned(_)) && self.checkpoint_sync
}

/// Validates the configured [`StorageMode`].
///
/// This must be called before opening the database, so that a misconfigured
Expand Down Expand Up @@ -433,6 +449,39 @@ impl Default for Config {
mod tests {
use super::*;

#[test]
fn skip_archive_indexes_only_when_pruned_and_checkpoint_syncing() {
let pruned_checkpoint = Config {
storage_mode: StorageMode::Pruned(PruningConfig::default()),
checkpoint_sync: true,
..Default::default()
};
assert!(
pruned_checkpoint.skip_archive_indexes(),
"pruned + checkpoint-sync skips archive-only indexes"
);

let archive = Config {
storage_mode: StorageMode::Archive,
checkpoint_sync: true,
..Default::default()
};
assert!(
!archive.skip_archive_indexes(),
"archive mode keeps archive-only indexes"
);

let pruned_legacy = Config {
storage_mode: StorageMode::Pruned(PruningConfig::default()),
checkpoint_sync: false,
..Default::default()
};
assert!(
!pruned_legacy.skip_archive_indexes(),
"pruned with checkpoint sync disabled keeps archive-only indexes"
);
}

#[test]
fn storage_mode_deserializes_from_documented_toml() {
let archive: Config = toml::from_str(r#"storage_mode = "archive""#)
Expand Down
73 changes: 53 additions & 20 deletions zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ use self::queued_blocks::{QueuedCheckpointVerified, QueuedSemanticallyVerified,

pub use self::traits::{ReadState, State};

/// Returned for transparent archive/indexer lookups when the required indexes were
/// not built. These indexes are RPC-only, not consensus.
const ARCHIVE_INDEXES_DISABLED: &str =
"the transparent archive indexes are disabled in pruned checkpoint-sync mode; \
getaddressbalance, getaddressutxos, getaddresstxids, and finalized transparent \
spender lookups require an archive node or full semantic verification";

/// A read-write service for Zebra's cached blockchain state.
///
/// This service modifies and provides access to:
Expand Down Expand Up @@ -1658,9 +1665,19 @@ impl Service<ReadRequest> for ReadStateService {
}

#[cfg(feature = "indexer")]
ReadRequest::SpendingTransactionId(spend) => Ok(ReadResponse::TransactionId(
read::spending_transaction_hash(state.latest_best_chain(), &state.db, spend),
)),
ReadRequest::SpendingTransactionId(spend) => {
let spending_transaction_hash =
read::spending_transaction_hash(state.latest_best_chain(), &state.db, spend);

if spending_transaction_hash.is_none()
&& matches!(spend, crate::request::Spend::OutPoint(_))
&& state.db.config().skip_archive_indexes()
{
Err(ARCHIVE_INDEXES_DISABLED.into())
} else {
Ok(ReadResponse::TransactionId(spending_transaction_hash))
}
}

ReadRequest::UnspentBestChainUtxo(outpoint) => Ok(ReadResponse::UnspentBestChainUtxo(
read::unspent_utxo(state.latest_best_chain(), &state.db, outpoint),
Expand Down Expand Up @@ -1807,31 +1824,47 @@ impl Service<ReadRequest> for ReadStateService {

// For the get_address_balance RPC.
ReadRequest::AddressBalance(addresses) => {
let (balance, received) =
read::transparent_balance(state.latest_best_chain(), &state.db, addresses)?;
Ok(ReadResponse::AddressBalance { balance, received })
if state.db.config().skip_archive_indexes() {
Err(ARCHIVE_INDEXES_DISABLED.into())
} else {
let (balance, received) =
read::transparent_balance(state.latest_best_chain(), &state.db, addresses)?;
Ok(ReadResponse::AddressBalance { balance, received })
}
}

// For the get_address_tx_ids RPC.
ReadRequest::TransactionIdsByAddresses {
addresses,
height_range,
} => read::transparent_tx_ids(
state.latest_best_chain(),
&state.db,
addresses,
height_range,
)
.map(ReadResponse::AddressesTransactionIds),
} => {
if state.db.config().skip_archive_indexes() {
Err(ARCHIVE_INDEXES_DISABLED.into())
} else {
read::transparent_tx_ids(
state.latest_best_chain(),
&state.db,
addresses,
height_range,
)
.map(ReadResponse::AddressesTransactionIds)
}
}

// For the get_address_utxos RPC.
ReadRequest::UtxosByAddresses(addresses) => read::address_utxos(
&state.network,
state.latest_best_chain(),
&state.db,
addresses,
)
.map(ReadResponse::AddressUtxos),
ReadRequest::UtxosByAddresses(addresses) => {
if state.db.config().skip_archive_indexes() {
Err(ARCHIVE_INDEXES_DISABLED.into())
} else {
read::address_utxos(
&state.network,
state.latest_best_chain(),
&state.db,
addresses,
)
.map(ReadResponse::AddressUtxos)
}
}

ReadRequest::CheckBestChainTipNullifiersAndAnchors(unmined_tx) => {
let latest_non_finalized_best_chain = state.latest_best_chain();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,14 @@ impl DiskFormatUpgrade for Upgrade {
return Err(CancelFormatChange);
}

// A pruned, checkpoint-syncing node does not build transparent archive-only
// indexes ([`Config::skip_archive_indexes`]), so the received-balance check below
// does not apply — the balances are intentionally absent. The BlockInfo check
// above still runs, since block info is written regardless of the address index.
if db.config().skip_archive_indexes() {
return Ok(Ok(()));
}

// Check that all recipient addresses of transparent transfers in the range have a non-zero received balance.

// Collect the set of addresses that received transparent funds in the last query range (last 1000 blocks).
Expand Down
54 changes: 32 additions & 22 deletions zebra-state/src/service/finalized_state/zebra_db/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -886,21 +886,6 @@ impl ZebraDb {
.map(|(_outpoint, out_loc, utxo)| (out_loc, utxo))
.collect();

// Get the transparent addresses with changed balances/UTXOs
let changed_addresses: HashSet<transparent::Address> = spent_utxos_by_out_loc
.values()
.chain(
finalized
.new_outputs
.values()
.map(|ordered_utxo| &ordered_utxo.utxo),
)
.filter_map(|utxo| utxo.output.address(network))
.unique()
.collect();

// Get the current address balances, before the transactions in this block

// Like the spent-UTXO reads above, the per-address balance lookups are
// cache-served but serial. Fan them across the rayon pool once a block
// touches enough addresses to amortize the fork-join cost.
Expand Down Expand Up @@ -934,14 +919,39 @@ impl ZebraDb {
// reading all of the pending merge operands (potentially hundreds), and applying pending merge operands to the
// fully-merged value such that it's much faster to read entries that have been updated with insertions than it
// is to read entries that have been updated with merge operations.
let address_balances: AddressBalanceLocationUpdates = if self.finished_format_upgrades() {
AddressBalanceLocationUpdates::Insert(read_addr_locs(changed_addresses, |addr| {
self.address_balance_location(addr)
}))
//
// When archive-only indexes are skipped (pruned + checkpoint-sync fast-validator),
// none of the per-address balance reads happen and `address_balances` is left
// empty; the gated transparent index passes below then write no address entries.
let address_balances: AddressBalanceLocationUpdates = if self
.config()
.skip_archive_indexes()
{
AddressBalanceLocationUpdates::Insert(HashMap::new())
} else {
AddressBalanceLocationUpdates::Merge(read_addr_locs(changed_addresses, |addr| {
Some(self.address_balance_location(addr)?.into_new_change())
}))
// Transparent addresses with changed balances/UTXOs in this block.
let changed_addresses: HashSet<transparent::Address> = spent_utxos_by_out_loc
.values()
.chain(
finalized
.new_outputs
.values()
.map(|ordered_utxo| &ordered_utxo.utxo),
)
.filter_map(|utxo| utxo.output.address(network))
.unique()
.collect();

// Get the current address balances, before the transactions in this block.
if self.finished_format_upgrades() {
AddressBalanceLocationUpdates::Insert(read_addr_locs(changed_addresses, |addr| {
self.address_balance_location(addr)
}))
} else {
AddressBalanceLocationUpdates::Merge(read_addr_locs(changed_addresses, |addr| {
Some(self.address_balance_location(addr)?.into_new_change())
}))
}
};

let mut batch = DiskWriteBatch::new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,74 @@ fn new_state_with_blocks(config: &Config, network: &Network) -> FinalizedState {
state
}

/// Returns the number of entries in the column family `cf_name`.
fn cf_len(state: &FinalizedState, cf_name: &str) -> usize {
use crate::service::finalized_state::disk_format::RawBytes;
let cf = state
.db
.db
.cf_handle(cf_name)
.expect("column family exists");
state
.db
.db
.zs_forward_range_iter::<_, RawBytes, RawBytes, _>(&cf, ..)
.count()
}

/// A pruned + checkpoint-syncing node commits the UTXO set but skips transparent
/// archive-only index column families; an archive node populates all of them.
#[test]
fn pruned_checkpoint_commit_skips_the_transparent_address_index() {
let _init_guard = zebra_test::init();
let network = Mainnet;

const BALANCE_CF: &str = "balance_by_transparent_addr";
const UTXO_LOC_CF: &str = "utxo_loc_by_transparent_addr_loc";
const TX_LOC_CF: &str = "tx_loc_by_transparent_addr_loc";
const SPENT_TX_LOC_CF: &str = "tx_loc_by_spent_out_loc";
const UTXO_SET_CF: &str = "utxo_by_out_loc";

// Archive mode keeps the address index: the early coinbase outputs populate all
// three address column families.
let archive = new_state_with_blocks(&Config::ephemeral(), &network);
assert!(
!archive.db.config().skip_archive_indexes(),
"archive mode keeps transparent archive-only indexes"
);
assert!(
cf_len(&archive, BALANCE_CF) > 0
&& cf_len(&archive, UTXO_LOC_CF) > 0
&& cf_len(&archive, TX_LOC_CF) > 0,
"archive commit populates the address index: balance={}, utxo_loc={}, tx_loc={}",
cf_len(&archive, BALANCE_CF),
cf_len(&archive, UTXO_LOC_CF),
cf_len(&archive, TX_LOC_CF),
);

// Pruned + checkpoint-sync skips archive-only indexes entirely, but still writes
// the consensus-critical UTXO set.
let pruned = new_state_with_blocks(&pruned_config(), &network);
assert!(
pruned.db.config().skip_archive_indexes(),
"pruned + checkpoint-sync skips transparent archive-only indexes"
);
assert_eq!(
(
cf_len(&pruned, BALANCE_CF),
cf_len(&pruned, UTXO_LOC_CF),
cf_len(&pruned, TX_LOC_CF),
cf_len(&pruned, SPENT_TX_LOC_CF),
),
(0, 0, 0, 0),
"pruned + checkpoint-sync writes no transparent archive-only index entries"
);
assert!(
cf_len(&pruned, UTXO_SET_CF) > 0,
"the UTXO set is still written when the address index is skipped"
);
}

/// Opens a fresh finalized state with a checkpoint retention start and commits
/// blocks `0..=TEST_BLOCKS` for `network`.
fn new_state_with_checkpoint_retention(
Expand Down
Loading