Add transaction IDs to the chain tip channel (#2686)
* Re-use finalized blocks for chain tip updates This avoids serializing and deserializing blocks from the finalized state. * Optimise tip sender equality checks * Re-use precalculated block hashes and heights for chain tip updates * Add chain tip mined transaction IDs * Doc comment typo Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> Co-authored-by: Deirdre Connolly <deirdre@zfnd.org> Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com>
This commit is contained in:
parent
424095096a
commit
2e1d857b27
|
@ -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<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<transaction::Hash>;
|
||||
}
|
||||
|
||||
/// A chain tip that is always empty.
|
||||
|
@ -27,4 +33,8 @@ impl ChainTip for NoChainTip {
|
|||
fn best_tip_hash(&self) -> Option<block::Hash> {
|
||||
None
|
||||
}
|
||||
|
||||
fn best_tip_mined_transaction_ids(&self) -> Vec<transaction::Hash> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Result<block::Hash, BoxError>> {
|
||||
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<Block>`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<Arc<Block>> {
|
||||
self.mem
|
||||
.best_block(hash_or_height)
|
||||
.map(|contextual| contextual.block)
|
||||
.or_else(|| self.disk.block(hash_or_height))
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Arc<Block>>;
|
||||
type ChainTipData = Option<ChainTipBlock>;
|
||||
|
||||
/// 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<Block>,
|
||||
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<transaction::Hash>,
|
||||
}
|
||||
|
||||
impl From<ContextuallyValidBlock> 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<FinalizedBlock> 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<Option<Arc<Block>>>) -> (Self, ChainTipReceiver) {
|
||||
pub fn new(initial_tip: impl Into<Option<ChainTipBlock>>) -> (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<Option<Arc<Block>>>) {
|
||||
pub fn set_finalized_tip(&mut self, new_tip: impl Into<Option<ChainTipBlock>>) {
|
||||
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<Option<Arc<Block>>>) {
|
||||
pub fn set_best_non_finalized_tip(&mut self, new_tip: impl Into<Option<ChainTipBlock>>) {
|
||||
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<Option<Arc<Block>>>) {
|
||||
fn update(&mut self, new_tip: impl Into<Option<ChainTipBlock>>) {
|
||||
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<block::Height> {
|
||||
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<block::Hash> {
|
||||
// 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<transaction::Hash> {
|
||||
self.receiver
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|block| block.transaction_hashes.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<Arc<Block>> {
|
||||
/// Returns the [`ContextuallyValidBlock`] at a given height or hash in the best chain.
|
||||
pub fn best_block(&self, hash_or_height: HashOrHeight) -> Option<ContextuallyValidBlock> {
|
||||
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<Arc<Block>> {
|
||||
pub fn best_tip_block(&self) -> Option<ContextuallyValidBlock> {
|
||||
let (height, _hash) = self.best_tip()?;
|
||||
self.best_block(height.into())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue