Reset `ChainTipChange`s on chain fork and network upgrade activation (#2721)

* Use `TipAction::Reset` for initialization and out-of-order blocks

Needs tests for:
- cloning a `ChainTipChange` resets the cloned instance
- skipped updates reset the cloned instance
- changing forks resets the cloned instance

* Use `TipAction::Reset` for network upgrade activation blocks

* Use an `if` expression

Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com>

* Another if expression

Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com>
This commit is contained in:
teor 2021-09-02 12:25:42 +10:00 committed by GitHub
parent 1ccb2de7c7
commit 44ac06775b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 186 additions and 41 deletions

View File

@ -208,6 +208,14 @@ impl NetworkUpgrade {
.next()
}
/// Returns `true` if `height` is the activation height of any network upgrade
/// on `network`.
///
/// Use [`activation_height`] to get the specific network upgrade.
pub fn is_activation_height(network: Network, height: block::Height) -> bool {
NetworkUpgrade::activation_list(network).contains_key(&height)
}
/// Returns a BTreeMap of NetworkUpgrades and their ConsensusBranchIds.
///
/// Branch ids are the same for mainnet and testnet.

View File

@ -50,6 +50,11 @@ fn activation_extremes(network: Network) {
Some(&Genesis)
);
assert_eq!(Genesis.activation_height(network), Some(block::Height(0)));
assert!(NetworkUpgrade::is_activation_height(
network,
block::Height(0)
));
assert_eq!(NetworkUpgrade::current(network, block::Height(0)), Genesis);
assert_eq!(
NetworkUpgrade::next(network, block::Height(0)),
@ -64,6 +69,11 @@ fn activation_extremes(network: Network) {
BeforeOverwinter.activation_height(network),
Some(block::Height(1))
);
assert!(NetworkUpgrade::is_activation_height(
network,
block::Height(1)
));
assert_eq!(
NetworkUpgrade::current(network, block::Height(1)),
BeforeOverwinter
@ -73,12 +83,22 @@ fn activation_extremes(network: Network) {
Some(Overwinter)
);
assert!(!NetworkUpgrade::is_activation_height(
network,
block::Height(2)
));
// We assume that the last upgrade we know about continues forever
// (even if we suspect that won't be true)
assert_ne!(
NetworkUpgrade::activation_list(network).get(&block::Height::MAX),
Some(&Genesis)
);
assert!(!NetworkUpgrade::is_activation_height(
network,
block::Height::MAX
));
assert_ne!(
NetworkUpgrade::current(network, block::Height::MAX),
Genesis
@ -98,8 +118,8 @@ fn activation_consistent_testnet() {
activation_consistent(Testnet)
}
/// Check that the activation_height, current, and next functions are consistent
/// for `network`.
/// Check that the `activation_height`, `is_activation_height`,
/// `current`, and `next` functions are consistent for `network`.
fn activation_consistent(network: Network) {
let activation_list = NetworkUpgrade::activation_list(network);
let network_upgrades: HashSet<&NetworkUpgrade> = activation_list.values().collect();
@ -108,6 +128,17 @@ fn activation_consistent(network: Network) {
let height = network_upgrade
.activation_height(network)
.expect("activations must have a height");
assert!(NetworkUpgrade::is_activation_height(network, height));
if height > block::Height(0) {
// Genesis is immediately followed by BeforeOverwinter,
// but the other network upgrades have multiple blocks between them
assert!(!NetworkUpgrade::is_activation_height(
network,
(height + 1).unwrap()
));
}
assert_eq!(NetworkUpgrade::current(network, height), network_upgrade);
// Network upgrades don't repeat
assert_ne!(NetworkUpgrade::next(network, height), Some(network_upgrade));

View File

@ -45,7 +45,7 @@ where
impl From<PreparedBlock> for ChainTipBlock {
fn from(prepared: PreparedBlock) -> Self {
let PreparedBlock {
block: _,
block,
hash,
height,
new_outputs: _,
@ -55,6 +55,7 @@ impl From<PreparedBlock> for ChainTipBlock {
hash,
height,
transaction_hashes,
previous_block_hash: block.header.previous_block_hash,
}
}
}

View File

@ -83,7 +83,7 @@ impl StateService {
.map(FinalizedBlock::from)
.map(ChainTipBlock::from);
let (chain_tip_sender, latest_chain_tip, chain_tip_change) =
ChainTipSender::new(initial_tip);
ChainTipSender::new(initial_tip, network);
let mem = NonFinalizedState::new(network);
let queued_blocks = QueuedBlocks::default();

View File

@ -9,7 +9,12 @@ use std::sync::Arc;
use tokio::sync::watch;
use zebra_chain::{block, chain_tip::ChainTip, transaction};
use zebra_chain::{
block,
chain_tip::ChainTip,
parameters::{Network, NetworkUpgrade},
transaction,
};
use crate::{request::ContextuallyValidBlock, FinalizedBlock};
@ -37,12 +42,21 @@ pub struct ChainTipBlock {
/// The mined transaction IDs of the transactions in `block`,
/// in the same order as `block.transactions`.
pub transaction_hashes: Arc<[transaction::Hash]>,
/// The hash of the previous block in the best chain.
/// This block is immediately behind the best chain tip.
///
/// ## Note
///
/// If the best chain fork has changed, or some blocks have been skipped,
/// this hash will be different to the last returned `ChainTipBlock.hash`.
pub(crate) previous_block_hash: block::Hash,
}
impl From<ContextuallyValidBlock> for ChainTipBlock {
fn from(contextually_valid: ContextuallyValidBlock) -> Self {
let ContextuallyValidBlock {
block: _,
block,
hash,
height,
new_outputs: _,
@ -53,6 +67,7 @@ impl From<ContextuallyValidBlock> for ChainTipBlock {
hash,
height,
transaction_hashes,
previous_block_hash: block.header.previous_block_hash,
}
}
}
@ -60,7 +75,7 @@ impl From<ContextuallyValidBlock> for ChainTipBlock {
impl From<FinalizedBlock> for ChainTipBlock {
fn from(finalized: FinalizedBlock) -> Self {
let FinalizedBlock {
block: _,
block,
hash,
height,
new_outputs: _,
@ -70,6 +85,7 @@ impl From<FinalizedBlock> for ChainTipBlock {
hash,
height,
transaction_hashes,
previous_block_hash: block.header.previous_block_hash,
}
}
}
@ -93,9 +109,10 @@ pub struct ChainTipSender {
impl ChainTipSender {
/// Create new linked instances of [`ChainTipSender`], [`LatestChainTip`], and [`ChainTipChange`],
/// using `initial_tip` as the tip.
/// using an `initial_tip` and a [`Network`].
pub fn new(
initial_tip: impl Into<Option<ChainTipBlock>>,
network: Network,
) -> (Self, LatestChainTip, ChainTipChange) {
let (sender, receiver) = watch::channel(None);
@ -106,7 +123,7 @@ impl ChainTipSender {
};
let current = LatestChainTip::new(receiver.clone());
let change = ChainTipChange::new(receiver);
let change = ChainTipChange::new(receiver, network);
sender.update(initial_tip);
@ -227,8 +244,16 @@ pub struct ChainTipChange {
/// The receiver for the current chain tip's data.
receiver: watch::Receiver<ChainTipData>,
/// The most recent hash provided by this instance.
previous_change_hash: Option<block::Hash>,
/// The most recent [`block::Hash`] provided by this instance.
///
/// ## Note
///
/// If the best chain fork has changed, or some blocks have been skipped,
/// this hash will be different to the last returned `ChainTipBlock.hash`.
last_change_hash: Option<block::Hash>,
/// The network for the chain tip.
network: Network,
}
/// Actions that we can take in response to a [`ChainTipChange`].
@ -288,18 +313,64 @@ impl ChainTipChange {
pub async fn tip_change(&mut self) -> Result<TipAction, watch::error::RecvError> {
let block = self.tip_block_change().await?;
// TODO: handle resets here
let action = self.action(block.clone());
self.previous_change_hash = Some(block.hash);
self.last_change_hash = Some(block.hash);
Ok(Grow { block })
Ok(action)
}
/// Create a new [`ChainTipChange`] from a watch channel receiver.
fn new(receiver: watch::Receiver<ChainTipData>) -> Self {
/// Return an action based on `block` and the last change we returned.
fn action(&self, block: ChainTipBlock) -> TipAction {
// check for an edge case that's dealt with by other code
assert!(
Some(block.hash) != self.last_change_hash,
"ChainTipSender ignores unchanged tips"
);
// If the previous block hash doesn't match, reset.
// We've either:
// - just initialized this instance,
// - changed the best chain to another fork (a rollback), or
// - skipped some blocks in the best chain.
//
// Consensus rules:
//
// > It is possible for a reorganization to occur
// > that rolls back from after the activation height, to before that height.
// > This can handled in the same way as any regular chain orphaning or reorganization,
// > as long as the new chain is valid.
//
// https://zips.z.cash/zip-0200#chain-reorganization
// If we're at a network upgrade activation block, reset.
//
// Consensus rules:
//
// > When the current chain tip height reaches ACTIVATION_HEIGHT,
// > the node's local transaction memory pool SHOULD be cleared of transactions
// > that will never be valid on the post-upgrade consensus branch.
//
// https://zips.z.cash/zip-0200#memory-pool
//
// Skipped blocks can include network upgrade activation blocks.
// Fork changes can activate or deactivate a network upgrade.
// So we must perform the same actions for network upgrades and skipped blocks.
if Some(block.previous_block_hash) != self.last_change_hash
|| NetworkUpgrade::is_activation_height(self.network, block.height)
{
TipAction::reset_with(block)
} else {
TipAction::grow_with(block)
}
}
/// Create a new [`ChainTipChange`] from a watch channel receiver and [`Network`].
fn new(receiver: watch::Receiver<ChainTipData>, network: Network) -> Self {
Self {
receiver,
previous_change_hash: None,
last_change_hash: None,
network,
}
}
@ -318,11 +389,6 @@ impl ChainTipChange {
// Wait until there is actually Some block,
// so we don't have `Option`s inside `TipAction`s.
if let Some(block) = self.best_tip_block() {
assert!(
Some(block.hash) != self.previous_change_hash,
"ChainTipSender must ignore unchanged tips"
);
return Ok(block);
}
}
@ -339,8 +405,11 @@ impl Clone for ChainTipChange {
fn clone(&self) -> Self {
Self {
receiver: self.receiver.clone(),
// clear the previous change hash, so the first action is a reset
previous_change_hash: None,
last_change_hash: None,
network: self.network,
}
}
}
@ -359,4 +428,17 @@ impl TipAction {
Reset { hash, .. } => *hash,
}
}
/// Returns a [`Grow`] based on `block`.
pub(crate) fn grow_with(block: ChainTipBlock) -> Self {
Grow { block }
}
/// Returns a [`Reset`] based on `block`.
pub(crate) fn reset_with(block: ChainTipBlock) -> Self {
Reset {
height: block.height,
hash: block.hash,
}
}
}

View File

@ -4,13 +4,15 @@ use futures::FutureExt;
use proptest::prelude::*;
use proptest_derive::Arbitrary;
use zebra_chain::{block::Block, chain_tip::ChainTip};
use crate::{
service::chain_tip::{ChainTipBlock, ChainTipSender, TipAction::*},
FinalizedBlock,
use zebra_chain::{
block::Block,
chain_tip::ChainTip,
fmt::{DisplayToDebug, SummaryDebug},
parameters::Network,
};
use crate::service::chain_tip::{ChainTipBlock, ChainTipSender, TipAction};
const DEFAULT_BLOCK_VEC_PROPTEST_CASES: u32 = 4;
proptest! {
@ -25,9 +27,10 @@ proptest! {
/// or otherwise the finalized tip.
#[test]
fn best_tip_is_latest_non_finalized_then_latest_finalized(
tip_updates in any::<Vec<BlockUpdate>>(),
tip_updates in any::<SummaryDebug<Vec<BlockUpdate>>>(),
network in any::<Network>(),
) {
let (mut chain_tip_sender, latest_chain_tip, mut chain_tip_change) = ChainTipSender::new(None);
let (mut chain_tip_sender, latest_chain_tip, mut chain_tip_change) = ChainTipSender::new(None, network);
let mut latest_finalized_tip = None;
let mut latest_non_finalized_tip = None;
@ -36,14 +39,14 @@ proptest! {
for update in tip_updates {
match update {
BlockUpdate::Finalized(block) => {
let chain_tip = block.clone().map(FinalizedBlock::from).map(ChainTipBlock::from);
let chain_tip = block.clone().map(|block| ChainTipBlock::from(block.0));
chain_tip_sender.set_finalized_tip(chain_tip.clone());
if let Some(block) = block {
latest_finalized_tip = Some((chain_tip, block));
}
}
BlockUpdate::NonFinalized(block) => {
let chain_tip = block.clone().map(FinalizedBlock::from).map(ChainTipBlock::from);
let chain_tip = block.clone().map(|block| ChainTipBlock::from(block.0));
chain_tip_sender.set_best_non_finalized_tip(chain_tip.clone());
if let Some(block) = block {
latest_non_finalized_tip = Some((chain_tip, block));
@ -101,13 +104,13 @@ proptest! {
.now_or_never()
.transpose()
.expect("watch sender is not dropped"),
expected_tip.map(|(_chain_tip, block)| Grow { block: block.into() })
expected_tip.map(|(_chain_tip, block)| TipAction::reset_with(block.0.into()))
);
}
}
#[derive(Arbitrary, Clone, Debug)]
enum BlockUpdate {
Finalized(Option<Arc<Block>>),
NonFinalized(Option<Arc<Block>>),
Finalized(Option<DisplayToDebug<Arc<Block>>>),
NonFinalized(Option<DisplayToDebug<Arc<Block>>>),
}

View File

@ -2,13 +2,17 @@ use std::iter;
use futures::FutureExt;
use zebra_chain::chain_tip::{ChainTip, NoChainTip};
use zebra_chain::{
chain_tip::{ChainTip, NoChainTip},
parameters::Network::*,
};
use super::super::ChainTipSender;
#[test]
fn current_best_tip_is_initially_empty() {
let (_chain_tip_sender, latest_chain_tip, _chain_tip_change) = ChainTipSender::new(None);
let (_chain_tip_sender, latest_chain_tip, _chain_tip_change) =
ChainTipSender::new(None, Mainnet);
assert_eq!(latest_chain_tip.best_tip_height(), None);
assert_eq!(latest_chain_tip.best_tip_hash(), None);
@ -32,7 +36,8 @@ fn empty_latest_chain_tip_is_empty() {
#[test]
fn chain_tip_change_is_initially_not_ready() {
let (_chain_tip_sender, _latest_chain_tip, mut chain_tip_change) = ChainTipSender::new(None);
let (_chain_tip_sender, _latest_chain_tip, mut chain_tip_change) =
ChainTipSender::new(None, Mainnet);
let first = chain_tip_change
.tip_change()

View File

@ -17,7 +17,7 @@ use zebra_test::{prelude::*, transcript::Transcript};
use crate::{
arbitrary::Prepare,
constants, init_test,
service::{chain_tip::TipAction::*, StateService},
service::{chain_tip::TipAction, StateService},
tests::setup::{partial_nu5_chain_strategy, transaction_v4_from_coinbase},
BoxError, Config, FinalizedBlock, PreparedBlock, Request, Response,
};
@ -312,6 +312,14 @@ proptest! {
for block in finalized_blocks {
let expected_block = block.clone();
let expected_action = if expected_block.height <= block::Height(1) {
// 0: reset by both initialization and the Genesis network upgrade
// 1: reset by the BeforeOverwinter network upgrade
TipAction::reset_with(expected_block.clone().into())
} else {
TipAction::grow_with(expected_block.clone().into())
};
state_service.queue_and_commit_finalized(block);
prop_assert_eq!(latest_chain_tip.best_tip_height(), Some(expected_block.height));
@ -321,13 +329,20 @@ proptest! {
.now_or_never()
.transpose()
.expect("watch sender is not dropped"),
Some(Grow { block: expected_block.into() })
Some(expected_action)
);
}
for block in non_finalized_blocks {
let expected_block = block.clone();
let expected_action = if expected_block.height == block::Height(1) {
// 1: reset by the BeforeOverwinter network upgrade
TipAction::reset_with(expected_block.clone().into())
} else {
TipAction::grow_with(expected_block.clone().into())
};
state_service.queue_and_commit_non_finalized(block);
prop_assert_eq!(latest_chain_tip.best_tip_height(), Some(expected_block.height));
@ -337,7 +352,7 @@ proptest! {
.now_or_never()
.transpose()
.expect("watch sender is not dropped"),
Some(Grow { block: expected_block.into() })
Some(expected_action)
);
}
}