diff --git a/zebra-chain/src/chain_tip.rs b/zebra-chain/src/chain_tip.rs index 5523e603..ab71f9b5 100644 --- a/zebra-chain/src/chain_tip.rs +++ b/zebra-chain/src/chain_tip.rs @@ -1,6 +1,6 @@ //! Chain tip interfaces. -use crate::block; +use crate::{block, transaction}; /// An interface for querying the chain tip. /// @@ -13,6 +13,12 @@ pub trait ChainTip { /// Return the block hash of the best chain tip. fn best_tip_hash(&self) -> Option; + + /// Return the mined transaction IDs of the transactions in the best chain tip block. + /// + /// All transactions with these mined IDs should be rejected from the mempool, + /// even if their authorizing data is different. + fn best_tip_mined_transaction_ids(&self) -> Vec; } /// A chain tip that is always empty. @@ -27,4 +33,8 @@ impl ChainTip for NoChainTip { fn best_tip_hash(&self) -> Option { None } + + fn best_tip_mined_transaction_ids(&self) -> Vec { + Vec::new() + } } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 902227ae..a752e57b 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -23,8 +23,9 @@ use zebra_chain::{ }; use crate::{ - constants, request::HashOrHeight, BoxError, CloneError, CommitBlockError, Config, - FinalizedBlock, PreparedBlock, Request, Response, ValidateContextError, + constants, request::HashOrHeight, service::chain_tip::ChainTipBlock, BoxError, CloneError, + CommitBlockError, Config, FinalizedBlock, PreparedBlock, Request, Response, + ValidateContextError, }; use self::{ @@ -77,7 +78,11 @@ impl StateService { pub fn new(config: Config, network: Network) -> (Self, ChainTipReceiver) { let disk = FinalizedState::new(&config, network); - let (chain_tip_sender, chain_tip_receiver) = ChainTipSender::new(disk.tip_block()); + let initial_tip = disk + .tip_block() + .map(FinalizedBlock::from) + .map(ChainTipBlock::from); + let (chain_tip_sender, chain_tip_receiver) = ChainTipSender::new(initial_tip); let mem = NonFinalizedState::new(network); let queued_blocks = QueuedBlocks::default(); @@ -127,11 +132,11 @@ impl StateService { ) -> oneshot::Receiver> { let (rsp_tx, rsp_rx) = oneshot::channel(); - self.disk.queue_and_commit_finalized((finalized, rsp_tx)); - // TODO: move into the finalized state, - // so we can clone committed `Arc`s before they get dropped - self.chain_tip_sender - .set_finalized_tip(self.disk.tip_block()); + let tip_block = self + .disk + .queue_and_commit_finalized((finalized, rsp_tx)) + .map(ChainTipBlock::from); + self.chain_tip_sender.set_finalized_tip(tip_block); rsp_rx } @@ -195,8 +200,8 @@ impl StateService { ); self.queued_blocks.prune_by_height(finalized_tip_height); - self.chain_tip_sender - .set_best_non_finalized_tip(self.mem.best_tip_block()); + let tip_block = self.mem.best_tip_block().map(ChainTipBlock::from); + self.chain_tip_sender.set_best_non_finalized_tip(tip_block); tracing::trace!("finished processing queued block"); rsp_rx @@ -322,6 +327,7 @@ impl StateService { pub fn best_block(&self, hash_or_height: HashOrHeight) -> Option> { self.mem .best_block(hash_or_height) + .map(|contextual| contextual.block) .or_else(|| self.disk.block(hash_or_height)) } diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 4180f2fa..01f57a1f 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -5,13 +5,67 @@ use tokio::sync::watch; use zebra_chain::{ block::{self, Block}, chain_tip::ChainTip, + transaction, }; +use crate::{request::ContextuallyValidBlock, FinalizedBlock}; + #[cfg(test)] mod tests; /// The internal watch channel data type for [`ChainTipSender`] and [`ChainTipReceiver`]. -type ChainTipData = Option>; +type ChainTipData = Option; + +/// A chain tip block, with precalculated block data. +/// +/// Used to efficiently update the [`ChainTipSender`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ChainTipBlock { + pub(crate) block: Arc, + pub(crate) hash: block::Hash, + pub(crate) height: block::Height, + + /// The mined transaction IDs of the transactions in `block`, + /// in the same order as `block.transactions`. + pub(crate) transaction_hashes: Vec, +} + +impl From for ChainTipBlock { + fn from(contextually_valid: ContextuallyValidBlock) -> Self { + let ContextuallyValidBlock { + block, + hash, + height, + new_outputs: _, + transaction_hashes, + chain_value_pool_change: _, + } = contextually_valid; + Self { + block, + hash, + height, + transaction_hashes, + } + } +} + +impl From for ChainTipBlock { + fn from(finalized: FinalizedBlock) -> Self { + let FinalizedBlock { + block, + hash, + height, + new_outputs: _, + transaction_hashes, + } = finalized; + Self { + block, + hash, + height, + transaction_hashes, + } + } +} /// A sender for recent changes to the non-finalized and finalized chain tips. #[derive(Debug)] @@ -33,7 +87,7 @@ pub struct ChainTipSender { impl ChainTipSender { /// Create new linked instances of [`ChainTipSender`] and [`ChainTipReceiver`], /// using `initial_tip` as the tip. - pub fn new(initial_tip: impl Into>>) -> (Self, ChainTipReceiver) { + pub fn new(initial_tip: impl Into>) -> (Self, ChainTipReceiver) { let (sender, receiver) = watch::channel(None); let mut sender = ChainTipSender { non_finalized_tip: false, @@ -50,7 +104,7 @@ impl ChainTipSender { /// Update the current finalized tip. /// /// May trigger an update to the best tip. - pub fn set_finalized_tip(&mut self, new_tip: impl Into>>) { + pub fn set_finalized_tip(&mut self, new_tip: impl Into>) { if !self.non_finalized_tip { self.update(new_tip); } @@ -59,7 +113,7 @@ impl ChainTipSender { /// Update the current non-finalized tip. /// /// May trigger an update to the best tip. - pub fn set_best_non_finalized_tip(&mut self, new_tip: impl Into>>) { + pub fn set_best_non_finalized_tip(&mut self, new_tip: impl Into>) { let new_tip = new_tip.into(); // once the non-finalized state becomes active, it is always populated @@ -74,14 +128,18 @@ impl ChainTipSender { /// /// An update is only sent if the current best tip is different from the last best tip /// that was sent. - fn update(&mut self, new_tip: impl Into>>) { + fn update(&mut self, new_tip: impl Into>) { let new_tip = new_tip.into(); - if new_tip.is_none() { - return; - } + let needs_update = match (new_tip.as_ref(), self.active_value.as_ref()) { + // since the blocks have been contextually validated, + // we know their hashes cover all the block data + (Some(new_tip), Some(active_value)) => new_tip.hash != active_value.hash, + (Some(_new_tip), None) => true, + (None, _active_value) => false, + }; - if new_tip != self.active_value { + if needs_update { let _ = self.sender.send(new_tip.clone()); self.active_value = new_tip; } @@ -110,16 +168,23 @@ impl ChainTipReceiver { impl ChainTip for ChainTipReceiver { /// Return the height of the best chain tip. fn best_tip_height(&self) -> Option { - self.receiver - .borrow() - .as_ref() - .and_then(|block| block.coinbase_height()) + self.receiver.borrow().as_ref().map(|block| block.height) } /// Return the block hash of the best chain tip. fn best_tip_hash(&self) -> Option { - // TODO: get the hash from the state and store it in the sender, - // so we don't have to recalculate it every time - self.receiver.borrow().as_ref().map(|block| block.hash()) + self.receiver.borrow().as_ref().map(|block| block.hash) + } + + /// Return the mined transaction IDs of the transactions in the best chain tip block. + /// + /// All transactions with these mined IDs should be rejected from the mempool, + /// even if their authorizing data is different. + fn best_tip_mined_transaction_ids(&self) -> Vec { + self.receiver + .borrow() + .as_ref() + .map(|block| block.transaction_hashes.clone()) + .unwrap_or_default() } } diff --git a/zebra-state/src/service/chain_tip/tests/prop.rs b/zebra-state/src/service/chain_tip/tests/prop.rs index e4a743de..0906dd4d 100644 --- a/zebra-state/src/service/chain_tip/tests/prop.rs +++ b/zebra-state/src/service/chain_tip/tests/prop.rs @@ -5,6 +5,8 @@ use proptest_derive::Arbitrary; use zebra_chain::{block::Block, chain_tip::ChainTip}; +use crate::{service::chain_tip::ChainTipBlock, FinalizedBlock}; + use super::super::ChainTipSender; const DEFAULT_BLOCK_VEC_PROPTEST_CASES: u32 = 4; @@ -32,12 +34,14 @@ proptest! { for update in tip_updates { match update { BlockUpdate::Finalized(block) => { + let block = block.map(FinalizedBlock::from).map(ChainTipBlock::from); chain_tip_sender.set_finalized_tip(block.clone()); if block.is_some() { latest_finalized_tip = block; } } BlockUpdate::NonFinalized(block) => { + let block = block.map(FinalizedBlock::from).map(ChainTipBlock::from); chain_tip_sender.set_best_non_finalized_tip(block.clone()); if block.is_some() { latest_non_finalized_tip = block; @@ -53,11 +57,22 @@ proptest! { latest_finalized_tip }; - let expected_height = expected_tip.as_ref().and_then(|block| block.coinbase_height()); + let expected_height = expected_tip.as_ref().and_then(|block| block.block.coinbase_height()); prop_assert_eq!(chain_tip_receiver.best_tip_height(), expected_height); - let expected_hash = expected_tip.as_ref().map(|block| block.hash()); + let expected_hash = expected_tip.as_ref().map(|block| block.block.hash()); prop_assert_eq!(chain_tip_receiver.best_tip_hash(), expected_hash); + + let expected_transaction_ids: Vec<_> = expected_tip + .as_ref() + .iter() + .flat_map(|block| block.block.transactions.clone()) + .map(|transaction| transaction.hash()) + .collect(); + prop_assert_eq!( + chain_tip_receiver.best_tip_mined_transaction_ids(), + expected_transaction_ids + ); } } diff --git a/zebra-state/src/service/chain_tip/tests/vectors.rs b/zebra-state/src/service/chain_tip/tests/vectors.rs index 0145cd7e..83e68cc8 100644 --- a/zebra-state/src/service/chain_tip/tests/vectors.rs +++ b/zebra-state/src/service/chain_tip/tests/vectors.rs @@ -8,6 +8,10 @@ fn best_tip_is_initially_empty() { assert_eq!(chain_tip_receiver.best_tip_height(), None); assert_eq!(chain_tip_receiver.best_tip_hash(), None); + assert_eq!( + chain_tip_receiver.best_tip_mined_transaction_ids(), + Vec::new() + ); } #[test] @@ -16,4 +20,8 @@ fn empty_chain_tip_is_empty() { assert_eq!(chain_tip_receiver.best_tip_height(), None); assert_eq!(chain_tip_receiver.best_tip_hash(), None); + assert_eq!( + chain_tip_receiver.best_tip_mined_transaction_ids(), + Vec::new() + ); } diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 8ca5fe33..c74c61bf 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -289,16 +289,13 @@ impl NonFinalizedState { None } - /// Returns the `block` at a given height or hash in the best chain. - pub fn best_block(&self, hash_or_height: HashOrHeight) -> Option> { + /// Returns the [`ContextuallyValidBlock`] at a given height or hash in the best chain. + pub fn best_block(&self, hash_or_height: HashOrHeight) -> Option { let best_chain = self.best_chain()?; let height = hash_or_height.height_or_else(|hash| best_chain.height_by_hash.get(&hash).cloned())?; - best_chain - .blocks - .get(&height) - .map(|prepared| prepared.block.clone()) + best_chain.blocks.get(&height).map(Clone::clone) } /// Returns the hash for a given `block::Height` if it is present in the best chain. @@ -319,7 +316,7 @@ impl NonFinalizedState { } /// Returns the block at the tip of the best chain. - pub fn best_tip_block(&self) -> Option> { + pub fn best_tip_block(&self) -> Option { let (height, _hash) = self.best_tip()?; self.best_block(height.into()) }