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

change(state): Stop re-downloading blocks that are in non-finalized side chains #6335

Merged
merged 15 commits into from
Mar 24, 2023
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
10 changes: 5 additions & 5 deletions zebra-consensus/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,15 @@ where
.ready()
.await
.map_err(|source| VerifyBlockError::Depth { source, hash })?
.call(zs::Request::Depth(hash))
.call(zs::Request::KnownBlock(hash))
.await
.map_err(|source| VerifyBlockError::Depth { source, hash })?
{
zs::Response::Depth(Some(depth)) => {
return Err(BlockError::AlreadyInChain(hash, depth).into())
zs::Response::KnownBlock(Some(location)) => {
return Err(BlockError::AlreadyInChain(hash, location).into())
}
zs::Response::Depth(None) => {}
_ => unreachable!("wrong response to Request::Depth"),
zs::Response::KnownBlock(None) => {}
_ => unreachable!("wrong response to Request::KnownBlock"),
}

tracing::trace!("performing block checks");
Expand Down
4 changes: 2 additions & 2 deletions zebra-consensus/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ pub enum BlockError {
#[error("block contains duplicate transactions")]
DuplicateTransaction,

#[error("block {0:?} is already in the chain at depth {1:?}")]
AlreadyInChain(zebra_chain::block::Hash, u32),
#[error("block {0:?} is already in present in the state {1:?}")]
AlreadyInChain(zebra_chain::block::Hash, zebra_state::KnownBlock),

#[error("invalid block {0:?}: missing block height")]
MissingHeight(zebra_chain::block::Hash),
Expand Down
2 changes: 1 addition & 1 deletion zebra-state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub use config::{check_and_delete_old_databases, Config};
pub use constants::MAX_BLOCK_REORG_HEIGHT;
pub use error::{BoxError, CloneError, CommitBlockError, ValidateContextError};
pub use request::{FinalizedBlock, HashOrHeight, PreparedBlock, ReadRequest, Request};
pub use response::{ReadResponse, Response};
pub use response::{KnownBlock, ReadResponse, Response};
pub use service::{
chain_tip::{ChainTipChange, LatestChainTip, TipAction},
init, spawn_init,
Expand Down
10 changes: 10 additions & 0 deletions zebra-state/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,13 @@ pub enum Request {
/// * [`Response::BlockHash(None)`](Response::BlockHash) otherwise.
BestChainBlockHash(block::Height),

/// Checks if a block is present anywhere in the state service.
/// Looks up `hash` in block queues as well as the finalized chain and all non-finalized chains.
///
/// Returns [`Response::KnownBlock(Some(Location))`](Response::KnownBlock) if the block is in the best state service.
/// Returns [`Response::KnownBlock(None)`](Response::KnownBlock) otherwise.
KnownBlock(block::Hash),

#[cfg(feature = "getblocktemplate-rpcs")]
/// Performs contextual validation of the given block, but does not commit it to the state.
///
Expand Down Expand Up @@ -634,6 +641,7 @@ impl Request {
}
Request::BestChainNextMedianTimePast => "best_chain_next_median_time_past",
Request::BestChainBlockHash(_) => "best_chain_block_hash",
Request::KnownBlock(_) => "known_block",
#[cfg(feature = "getblocktemplate-rpcs")]
Request::CheckBlockProposalValidity(_) => "check_block_proposal_validity",
}
Expand Down Expand Up @@ -947,6 +955,8 @@ impl TryFrom<Request> for ReadRequest {
Manually convert the request to ReadRequest::AnyChainUtxo, \
and handle pending UTXOs"),

Request::KnownBlock(_) => Err("ReadService does not track queued blocks"),

#[cfg(feature = "getblocktemplate-rpcs")]
Request::CheckBlockProposalValidity(prepared) => {
Ok(ReadRequest::CheckBlockProposalValidity(prepared))
Expand Down
16 changes: 16 additions & 0 deletions zebra-state/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,27 @@ pub enum Response {
/// specified block hash.
BlockHash(Option<block::Hash>),

/// Response to [`Request::KnownBlock`].
KnownBlock(Option<KnownBlock>),

#[cfg(feature = "getblocktemplate-rpcs")]
/// Response to [`Request::CheckBlockProposalValidity`](Request::CheckBlockProposalValidity)
ValidBlockProposal,
}

#[derive(Clone, Debug, PartialEq, Eq)]
/// An enum of block stores in the state where a block hash could be found.
pub enum KnownBlock {
/// Block is in the best chain.
BestChain,

/// Block is in a side chain.
SideChain,

/// Block is queued to be validated and committed, or rejected and dropped.
Queue,
}

#[derive(Clone, Debug, PartialEq, Eq)]
/// A response to a read-only
/// [`ReadStateService`](crate::service::ReadStateService)'s
Expand Down
30 changes: 26 additions & 4 deletions zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ pub(crate) struct StateService {
// - remove block hashes once their heights are strictly less than the finalized tip
last_sent_finalized_block_hash: block::Hash,

/// A set of non-finalized block hashes that have been sent to the block write task.
/// A set of block hashes that have been sent to the block write task.
/// Hashes of blocks below the finalized tip height are periodically pruned.
sent_non_finalized_block_hashes: SentHashes,

Expand Down Expand Up @@ -713,13 +713,13 @@ impl StateService {
return rsp_rx;
}

// Wait until block commit task is ready to write non-finalized blocks before dequeuing them
if self.finalized_block_write_sender.is_none() {
// Wait until block commit task is ready to write non-finalized blocks before dequeuing them
self.send_ready_non_finalized_queued(parent_hash);

let finalized_tip_height = self.read_service.db.finalized_tip_height().expect(
"Finalized state must have at least one block before committing non-finalized state",
);
"Finalized state must have at least one block before committing non-finalized state",
);

self.queued_non_finalized_blocks
.prune_by_height(finalized_tip_height);
Expand Down Expand Up @@ -1063,6 +1063,28 @@ impl Service<Request> for StateService {
.boxed()
}

// Used by sync, inbound, and block verifier to check if a block is already in the state
// before downloading or validating it.
Request::KnownBlock(hash) => {
let timer = CodeTimer::start();

let read_service = self.read_service.clone();

async move {
let response = read::non_finalized_state_contains_block_hash(
&read_service.latest_non_finalized_state(),
hash,
)
.or_else(|| read::finalized_state_contains_block_hash(&read_service.db, hash));

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

Ok(Response::KnownBlock(response))
}
.boxed()
}

// Runs concurrently using the ReadStateService
Request::Tip
| Request::Depth(_)
Expand Down
6 changes: 6 additions & 0 deletions zebra-state/src/service/non_finalized_state/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,12 @@ impl Chain {
self.height_by_hash.get(&hash).cloned()
}

/// Returns true is the chain contains the given block hash.
/// Returns false otherwise.
pub fn contains_block_hash(&self, hash: &block::Hash) -> bool {
self.height_by_hash.contains_key(hash)
}

/// Returns the non-finalized tip block height and hash.
pub fn non_finalized_tip(&self) -> (Height, block::Hash) {
(
Expand Down
3 changes: 3 additions & 0 deletions zebra-state/src/service/queued_blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ impl SentHashes {
/// Used for finalized blocks close to the final checkpoint, so non-finalized blocks can look up
/// their UTXOs.
///
/// Assumes that blocks are added in the order of their height between `finish_batch` calls
/// for efficient pruning.
///
/// For more details see `add()`.
pub fn add_finalized(&mut self, block: &FinalizedBlock) {
// Track known UTXOs in sent blocks.
Expand Down
5 changes: 3 additions & 2 deletions zebra-state/src/service/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ pub use block::{
any_utxo, block, block_header, transaction, transaction_hashes_for_block, unspent_utxo, utxo,
};
pub use find::{
best_tip, block_locator, chain_contains_hash, depth, find_chain_hashes, find_chain_headers,
hash_by_height, height_by_hash, next_median_time_past, tip, tip_height,
best_tip, block_locator, chain_contains_hash, depth, finalized_state_contains_block_hash,
find_chain_hashes, find_chain_headers, hash_by_height, height_by_hash, next_median_time_past,
non_finalized_state_contains_block_hash, tip, tip_height,
};
pub use tree::{orchard_tree, sapling_tree};

Expand Down
27 changes: 26 additions & 1 deletion zebra-state/src/service/read/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::{
non_finalized_state::{Chain, NonFinalizedState},
read::{self, block::block_header, FINALIZED_STATE_QUERY_RETRIES},
},
BoxError,
BoxError, KnownBlock,
};

#[cfg(test)]
Expand Down Expand Up @@ -101,6 +101,31 @@ where
Some(tip.0 - height.0)
}

/// Returns the location of the block if present in the non-finalized state.
/// Returns None if the block hash is not found in the non-finalized state.
pub fn non_finalized_state_contains_block_hash(
non_finalized_state: &NonFinalizedState,
hash: block::Hash,
) -> Option<KnownBlock> {
let mut chains_iter = non_finalized_state.chain_set.iter().rev();
let is_hash_in_chain = |chain: &Arc<Chain>| chain.contains_block_hash(&hash);

// Equivalent to `chain_set.iter().next_back()` in `NonFinalizedState.best_chain()` method.
let best_chain = chains_iter.next();

match best_chain.map(is_hash_in_chain) {
Some(true) => Some(KnownBlock::BestChain),
Some(false) if chains_iter.any(is_hash_in_chain) => Some(KnownBlock::SideChain),
Some(false) | None => None,
}
}

/// Returns the location of the block if present in the finalized state.
/// Returns None if the block hash is not found in the finalized state.
pub fn finalized_state_contains_block_hash(db: &ZebraDb, hash: block::Hash) -> Option<KnownBlock> {
db.contains_hash(hash).then_some(KnownBlock::BestChain)
}

/// Return the height for the block at `hash`, if `hash` is in `chain` or `db`.
pub fn height_by_hash<C>(chain: Option<C>, db: &ZebraDb, hash: block::Hash) -> Option<Height>
where
Expand Down
8 changes: 3 additions & 5 deletions zebrad/src/components/inbound/downloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,9 @@ where

let fut = async move {
// Check if the block is already in the state.
// BUG: check if the hash is in any chain (#862).
// Depth only checks the main chain.
match state.oneshot(zs::Request::Depth(hash)).await {
Ok(zs::Response::Depth(None)) => Ok(()),
Ok(zs::Response::Depth(Some(_))) => Err("already present".into()),
match state.oneshot(zs::Request::KnownBlock(hash)).await {
Ok(zs::Response::KnownBlock(None)) => Ok(()),
Ok(zs::Response::KnownBlock(Some(_))) => Err("already present".into()),
Ok(_) => unreachable!("wrong response"),
Err(e) => Err(e),
}?;
Expand Down
10 changes: 3 additions & 7 deletions zebrad/src/components/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1057,22 +1057,18 @@ where

/// Returns `true` if the hash is present in the state, and `false`
/// if the hash is not present in the state.
///
/// TODO BUG: check if the hash is in any chain (#862)
/// Depth only checks the main chain.
async fn state_contains(&mut self, hash: block::Hash) -> Result<bool, Report> {
match self
.state
.ready()
.await
.map_err(|e| eyre!(e))?
.call(zebra_state::Request::Depth(hash))
.call(zebra_state::Request::KnownBlock(hash))
.await
.map_err(|e| eyre!(e))?
{
zs::Response::Depth(Some(_)) => Ok(true),
zs::Response::Depth(None) => Ok(false),
_ => unreachable!("wrong response to depth request"),
zs::Response::KnownBlock(loc) => Ok(loc.is_some()),
_ => unreachable!("wrong response to known block request"),
}
}

Expand Down
4 changes: 2 additions & 2 deletions zebrad/src/components/sync/tests/timing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ fn request_genesis_is_rate_limited() {
// panic in any other type of request.
let state_service = tower::service_fn(move |request| {
match request {
zebra_state::Request::Depth(_) => {
zebra_state::Request::KnownBlock(_) => {
// Track the call
state_requests_counter_in_service.fetch_add(1, Ordering::SeqCst);
// Respond with `None`
future::ok(zebra_state::Response::Depth(None))
future::ok(zebra_state::Response::KnownBlock(None))
}
_ => unreachable!("no other request is allowed"),
}
Expand Down
Loading