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:
teor 2021-08-30 12:38:41 +10:00 committed by GitHub
parent 424095096a
commit 2e1d857b27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 137 additions and 36 deletions

View File

@ -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()
}
}

View File

@ -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))
}

View File

@ -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()
}
}

View File

@ -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
);
}
}

View File

@ -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()
);
}

View File

@ -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())
}