Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add(state): Track spending transaction ids by spent outpoints and revealed nullifiers #8895

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e9b3930
Adds new column family for [spent_out_loc] -> [spending_tx_loc] with …
arya2 Sep 25, 2024
5281565
add spending tx ids for spent outpoints to non-finalized chains
arya2 Sep 27, 2024
445a929
adds a `spending_transaction_hash()` read fn for the new column family
arya2 Sep 27, 2024
00b2eef
Adds a `TransactionIdForSpentOutPoint` ReadRequest and a `Transaction…
arya2 Sep 27, 2024
f6dcfbf
Updates snapshots, removes outdated TODOs, moves a TODO.
arya2 Sep 27, 2024
3b287bf
Clarifies `spent_utxos` field docs, fixes an assertion
arya2 Sep 27, 2024
c238bbb
import TypedColumnFamily from `finalized_state` instead of from the c…
arya2 Sep 30, 2024
2c6bac8
adds db format upgrade for spent outpoints -> tx hash
arya2 Oct 1, 2024
6903532
adds revealing tx ids for nullifiers in finalized and non-finalized s…
arya2 Oct 1, 2024
a02b307
updates nullifiers column families to include revaling transaction lo…
arya2 Oct 1, 2024
511ff82
Renames new read state request to `SpendingTransactionId` and updates…
arya2 Oct 4, 2024
ecb345a
refactor db format upgrade and prepare_nullifiers_batch() to use Zebr…
arya2 Oct 4, 2024
e20e791
Adds acceptance test for checking that the finalized state has spendi…
arya2 Oct 5, 2024
933556b
Adds variant docs to zebra_state::request::Spend enum
arya2 Oct 5, 2024
22c9289
Updates Zebra book with the latest changes to the rocks db column fam…
arya2 Oct 7, 2024
57c3cf9
Updates acceptance test to check non-finalized state
arya2 Oct 7, 2024
5ae6ff1
adds a few log messages to the acceptance test, reduces frequency of …
arya2 Oct 7, 2024
2db4dd5
fixes docs lint and skips test when there is no cached state
arya2 Oct 7, 2024
c184810
Avoids returning genesis coinbase tx hash when indexes are missing
arya2 Oct 18, 2024
39ede86
Adds `indexer` compilation feature in zebra-state and build metadata …
arya2 Oct 24, 2024
2585fd9
stops tracking new indexes in finalized state when feature is unselected
arya2 Oct 24, 2024
769e024
stops tracking new indexes in non-finalized state when indexer featur…
arya2 Oct 24, 2024
661316c
condenses imports
arya2 Oct 25, 2024
e6a8654
- adds build metadata when writing db version file, if any.
arya2 Oct 25, 2024
f57c784
Replaces dropping cf with deleting range of all items to avoid a pani…
arya2 Oct 25, 2024
527737a
Fixes lint, avoids reading coinbase transactions from disk
arya2 Oct 25, 2024
1ebb1a5
updates db column families table
arya2 Nov 19, 2024
f71c897
Document need for having an indexed cached state and use a multi-thre…
arya2 Nov 29, 2024
839cbab
Merge remote-tracking branch 'origin/main' into index-tx-loc-by-spent…
arya2 Dec 13, 2024
a280b60
fixes call to renamed `future_blocks` test fn
arya2 Dec 13, 2024
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
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6328,6 +6328,7 @@ dependencies = [
"bincode",
"chrono",
"color-eyre",
"crossbeam-channel",
arya2 marked this conversation as resolved.
Show resolved Hide resolved
"dirs",
"elasticsearch",
"futures",
Expand Down
13 changes: 13 additions & 0 deletions book/src/dev/state-db-upgrades.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,19 @@ We use the following rocksdb column families:
| `history_tree` | `()` | `NonEmptyHistoryTree` | Update |
| `tip_chain_value_pool` | `()` | `ValueBalance` | Update |

With the following additional modifications when compiled with the `indexer` feature:

| Column Family | Keys | Values | Changes |
| ---------------------------------- | ---------------------- | ----------------------------- | ------- |
| *Transparent* | | | |
| `tx_loc_by_spent_out_loc` | `OutputLocation` | `TransactionLocation` | Create |
| *Sprout* | | | |
| `sprout_nullifiers` | `sprout::Nullifier` | `TransactionLocation` | Create |
| *Sapling* | | | |
| `sapling_nullifiers` | `sapling::Nullifier` | `TransactionLocation` | Create |
| *Orchard* | | | |
| `orchard_nullifiers` | `orchard::Nullifier` | `TransactionLocation` | Create |

### Data Formats
[rocksdb-data-format]: #rocksdb-data-format

Expand Down
1 change: 1 addition & 0 deletions zebra-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ indexer-rpcs = [
"tonic-reflection",
"prost",
"tokio-stream",
"zebra-state/indexer"
]

# Production features that activate extra dependencies, or extra features in dependencies
Expand Down
4 changes: 4 additions & 0 deletions zebra-state/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ getblocktemplate-rpcs = [
"zebra-chain/getblocktemplate-rpcs",
]

# Indexes spending transaction ids by spent outpoints and revealed nullifiers
indexer = []

# Test-only features
proptest-impl = [
"proptest",
Expand Down Expand Up @@ -66,6 +69,7 @@ semver = "1.0.23"
serde = { version = "1.0.211", features = ["serde_derive"] }
tempfile = "3.13.0"
thiserror = "1.0.64"
crossbeam-channel = "0.5.13"

rayon = "1.10.0"
tokio = { version = "1.41.0", features = ["rt-multi-thread", "sync", "tracing"] }
Expand Down
16 changes: 6 additions & 10 deletions zebra-state/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,15 +431,7 @@ pub(crate) fn database_format_version_at_path(

// The database has a version file on disk
if let Some(version) = disk_version_file {
let (minor, patch) = version
.split_once('.')
.ok_or("invalid database format version file")?;

return Ok(Some(Version::new(
major_version,
minor.parse()?,
patch.parse()?,
)));
return Ok(Some(format!("{major_version}.{version}").parse()?));
}

// There's no version file on disk, so we need to guess the version
Expand Down Expand Up @@ -508,7 +500,11 @@ pub(crate) mod hidden {
) -> Result<(), BoxError> {
let version_path = config.version_file_path(db_kind, changed_version.major, network);

let version = format!("{}.{}", changed_version.minor, changed_version.patch);
let mut version = format!("{}.{}", changed_version.minor, changed_version.patch);

if !changed_version.build.is_empty() {
version.push_str(&format!("+{}", changed_version.build));
}

// Write the version file atomically so the cache is not corrupted if Zebra shuts down or
// crashes.
Expand Down
15 changes: 10 additions & 5 deletions zebra-state/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,16 @@ const DATABASE_FORMAT_PATCH_VERSION: u64 = 0;
/// This is the version implemented by the Zebra code that's currently running,
/// the minor and patch versions on disk can be different.
pub fn state_database_format_version_in_code() -> Version {
Version::new(
DATABASE_FORMAT_VERSION,
DATABASE_FORMAT_MINOR_VERSION,
DATABASE_FORMAT_PATCH_VERSION,
)
Version {
major: DATABASE_FORMAT_VERSION,
minor: DATABASE_FORMAT_MINOR_VERSION,
patch: DATABASE_FORMAT_PATCH_VERSION,
pre: semver::Prerelease::EMPTY,
#[cfg(feature = "indexer")]
build: semver::BuildMetadata::new("indexer").expect("hard-coded value should be valid"),
#[cfg(not(feature = "indexer"))]
build: semver::BuildMetadata::EMPTY,
}
}

/// Returns the highest database version that modifies the subtree index format.
Expand Down
4 changes: 4 additions & 0 deletions zebra-state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub use error::{
pub use request::{
CheckpointVerifiedBlock, HashOrHeight, ReadRequest, Request, SemanticallyVerifiedBlock,
};

#[cfg(feature = "indexer")]
pub use request::Spend;

pub use response::{KnownBlock, MinedTx, ReadResponse, Response};
pub use service::{
chain_tip::{ChainTipBlock, ChainTipChange, ChainTipSender, LatestChainTip, TipAction},
Expand Down
54 changes: 54 additions & 0 deletions zebra-state/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,51 @@ use crate::{
ReadResponse, Response,
};

/// Identify a spend by a transparent outpoint or revealed nullifier.
///
/// This enum implements `From` for [`transparent::OutPoint`], [`sprout::Nullifier`],
/// [`sapling::Nullifier`], and [`orchard::Nullifier`].
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg(feature = "indexer")]
pub enum Spend {
/// A spend identified by a [`transparent::OutPoint`].
OutPoint(transparent::OutPoint),
/// A spend identified by a [`sprout::Nullifier`].
Sprout(sprout::Nullifier),
/// A spend identified by a [`sapling::Nullifier`].
Sapling(sapling::Nullifier),
/// A spend identified by a [`orchard::Nullifier`].
Orchard(orchard::Nullifier),
}

#[cfg(feature = "indexer")]
impl From<transparent::OutPoint> for Spend {
fn from(outpoint: transparent::OutPoint) -> Self {
Self::OutPoint(outpoint)
}
}

#[cfg(feature = "indexer")]
impl From<sprout::Nullifier> for Spend {
fn from(sprout_nullifier: sprout::Nullifier) -> Self {
Self::Sprout(sprout_nullifier)
}
}

#[cfg(feature = "indexer")]
impl From<sapling::Nullifier> for Spend {
fn from(sapling_nullifier: sapling::Nullifier) -> Self {
Self::Sapling(sapling_nullifier)
}
}

#[cfg(feature = "indexer")]
impl From<orchard::Nullifier> for Spend {
fn from(orchard_nullifier: orchard::Nullifier) -> Self {
Self::Orchard(orchard_nullifier)
}
}

/// Identify a block by hash or height.
///
/// This enum implements `From` for [`block::Hash`] and [`block::Height`],
Expand Down Expand Up @@ -1020,6 +1065,13 @@ pub enum ReadRequest {
height_range: RangeInclusive<block::Height>,
},

/// Looks up a spending transaction id by its spent transparent input.
///
/// Returns [`ReadResponse::TransactionId`] with the hash of the transaction
/// that spent the output at the provided [`transparent::OutPoint`].
#[cfg(feature = "indexer")]
SpendingTransactionId(Spend),

/// Looks up utxos for the provided addresses.
///
/// Returns a type with found utxos and transaction information.
Expand Down Expand Up @@ -1106,6 +1158,8 @@ impl ReadRequest {
}
ReadRequest::BestChainNextMedianTimePast => "best_chain_next_median_time_past",
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
#[cfg(feature = "indexer")]
ReadRequest::SpendingTransactionId(_) => "spending_transaction_id",
#[cfg(feature = "getblocktemplate-rpcs")]
ReadRequest::ChainInfo => "chain_info",
#[cfg(feature = "getblocktemplate-rpcs")]
Expand Down
9 changes: 9 additions & 0 deletions zebra-state/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ pub enum ReadResponse {
/// or `None` if the block was not found.
TransactionIdsForBlock(Option<Arc<[transaction::Hash]>>),

/// Response to [`ReadRequest::SpendingTransactionId`],
/// with an list of transaction hashes in block order,
/// or `None` if the block was not found.
#[cfg(feature = "indexer")]
TransactionId(Option<transaction::Hash>),

/// Response to [`ReadRequest::BlockLocator`] with a block locator object.
BlockLocator(Vec<block::Hash>),

Expand Down Expand Up @@ -343,6 +349,9 @@ impl TryFrom<ReadResponse> for Response {
Err("there is no corresponding Response for this ReadResponse")
}

#[cfg(feature = "indexer")]
ReadResponse::TransactionId(_) => Err("there is no corresponding Response for this ReadResponse"),

#[cfg(feature = "getblocktemplate-rpcs")]
ReadResponse::ValidBlockProposal => Ok(Response::ValidBlockProposal),

Expand Down
29 changes: 29 additions & 0 deletions zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,35 @@ impl Service<ReadRequest> for ReadStateService {
.wait_for_panics()
}

#[cfg(feature = "indexer")]
ReadRequest::SpendingTransactionId(spend) => {
let state = self.clone();

tokio::task::spawn_blocking(move || {
span.in_scope(move || {
let spending_transaction_id = state
.non_finalized_state_receiver
.with_watch_data(|non_finalized_state| {
read::spending_transaction_hash(
non_finalized_state.best_chain(),
&state.db,
spend,
)
});

// The work is done in the future.
timer.finish(
module_path!(),
line!(),
"ReadRequest::TransactionIdForSpentOutPoint",
);

Ok(ReadResponse::TransactionId(spending_transaction_id))
})
})
.wait_for_panics()
}

ReadRequest::UnspentBestChainUtxo(outpoint) => {
let state = self.clone();

Expand Down
28 changes: 19 additions & 9 deletions zebra-state/src/service/check/nullifier.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
//! Checks for nullifier uniqueness.

use std::{collections::HashSet, sync::Arc};
use std::{collections::HashMap, sync::Arc};

use tracing::trace;
use zebra_chain::transaction::Transaction;

use crate::{
error::DuplicateNullifierError,
service::{finalized_state::ZebraDb, non_finalized_state::Chain},
service::{
finalized_state::ZebraDb,
non_finalized_state::{Chain, SpendingTransactionId},
},
SemanticallyVerifiedBlock, ValidateContextError,
};

Expand Down Expand Up @@ -105,19 +108,22 @@ pub(crate) fn tx_no_duplicates_in_chain(
find_duplicate_nullifier(
transaction.sprout_nullifiers(),
|nullifier| finalized_chain.contains_sprout_nullifier(nullifier),
non_finalized_chain.map(|chain| |nullifier| chain.sprout_nullifiers.contains(nullifier)),
non_finalized_chain
.map(|chain| |nullifier| chain.sprout_nullifiers.contains_key(nullifier)),
)?;

find_duplicate_nullifier(
transaction.sapling_nullifiers(),
|nullifier| finalized_chain.contains_sapling_nullifier(nullifier),
non_finalized_chain.map(|chain| |nullifier| chain.sapling_nullifiers.contains(nullifier)),
non_finalized_chain
.map(|chain| |nullifier| chain.sapling_nullifiers.contains_key(nullifier)),
)?;

find_duplicate_nullifier(
transaction.orchard_nullifiers(),
|nullifier| finalized_chain.contains_orchard_nullifier(nullifier),
non_finalized_chain.map(|chain| |nullifier| chain.orchard_nullifiers.contains(nullifier)),
non_finalized_chain
.map(|chain| |nullifier| chain.orchard_nullifiers.contains_key(nullifier)),
)?;

Ok(())
Expand Down Expand Up @@ -156,8 +162,9 @@ pub(crate) fn tx_no_duplicates_in_chain(
/// [5]: service::non_finalized_state::Chain
#[tracing::instrument(skip(chain_nullifiers, shielded_data_nullifiers))]
pub(crate) fn add_to_non_finalized_chain_unique<'block, NullifierT>(
chain_nullifiers: &mut HashSet<NullifierT>,
chain_nullifiers: &mut HashMap<NullifierT, SpendingTransactionId>,
shielded_data_nullifiers: impl IntoIterator<Item = &'block NullifierT>,
revealing_tx_id: SpendingTransactionId,
) -> Result<(), ValidateContextError>
where
NullifierT: DuplicateNullifierError + Copy + std::fmt::Debug + Eq + std::hash::Hash + 'block,
Expand All @@ -166,7 +173,10 @@ where
trace!(?nullifier, "adding nullifier");

// reject the nullifier if it is already present in this non-finalized chain
if !chain_nullifiers.insert(*nullifier) {
if chain_nullifiers
.insert(*nullifier, revealing_tx_id)
.is_some()
{
Err(nullifier.duplicate_nullifier_error(false))?;
}
}
Expand Down Expand Up @@ -200,7 +210,7 @@ where
/// [1]: service::non_finalized_state::Chain
#[tracing::instrument(skip(chain_nullifiers, shielded_data_nullifiers))]
pub(crate) fn remove_from_non_finalized_chain<'block, NullifierT>(
chain_nullifiers: &mut HashSet<NullifierT>,
chain_nullifiers: &mut HashMap<NullifierT, SpendingTransactionId>,
shielded_data_nullifiers: impl IntoIterator<Item = &'block NullifierT>,
) where
NullifierT: std::fmt::Debug + Eq + std::hash::Hash + 'block,
Expand All @@ -209,7 +219,7 @@ pub(crate) fn remove_from_non_finalized_chain<'block, NullifierT>(
trace!(?nullifier, "removing nullifier");

assert!(
chain_nullifiers.remove(nullifier),
chain_nullifiers.remove(nullifier).is_some(),
"nullifier must be present if block was added to chain"
);
}
Expand Down
10 changes: 5 additions & 5 deletions zebra-state/src/service/check/tests/utxo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ proptest! {
.unwrap();
prop_assert!(!chain.unspent_utxos().contains_key(&expected_outpoint));
prop_assert!(chain.created_utxos.contains_key(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains_key(&expected_outpoint));

// the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
Expand Down Expand Up @@ -310,14 +310,14 @@ proptest! {
if use_finalized_state_output {
// the chain has spent the UTXO from the finalized state
prop_assert!(!chain.created_utxos.contains_key(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains_key(&expected_outpoint));
// the finalized state has the UTXO, but it will get deleted on commit
prop_assert!(finalized_state.utxo(&expected_outpoint).is_some());
} else {
// the chain has spent its own UTXO
prop_assert!(!chain.unspent_utxos().contains_key(&expected_outpoint));
prop_assert!(chain.created_utxos.contains_key(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains_key(&expected_outpoint));
// the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
Expand Down Expand Up @@ -650,12 +650,12 @@ proptest! {
// the finalized state has the unspent UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_some());
// the non-finalized state has spent the UTXO
prop_assert!(chain.spent_utxos.contains(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains_key(&expected_outpoint));
} else {
// the non-finalized state has created and spent the UTXO
prop_assert!(!chain.unspent_utxos().contains_key(&expected_outpoint));
prop_assert!(chain.created_utxos.contains_key(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains(&expected_outpoint));
prop_assert!(chain.spent_utxos.contains_key(&expected_outpoint));
// the finalized state does not have the UTXO
prop_assert!(finalized_state.utxo(&expected_outpoint).is_none());
}
Expand Down
Loading
Loading