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 transaction IDs to the chain tip channel #2686

Merged
merged 6 commits into from
Aug 30, 2021
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
12 changes: 11 additions & 1 deletion zebra-chain/src/chain_tip.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Chain tip interfaces.

use crate::block;
use crate::{block, transaction};

/// An interface for querying the chain tip.
///
Expand All @@ -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.
Expand All @@ -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()
}
}
26 changes: 16 additions & 10 deletions zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}

Expand Down
97 changes: 81 additions & 16 deletions zebra-state/src/service/chain_tip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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,
Expand All @@ -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);
}
Expand All @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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()
}
}
19 changes: 17 additions & 2 deletions zebra-state/src/service/chain_tip/tests/prop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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
);
}
}

Expand Down
8 changes: 8 additions & 0 deletions zebra-state/src/service/chain_tip/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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()
);
}
11 changes: 4 additions & 7 deletions zebra-state/src/service/non_finalized_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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())
}
Expand Down