Skip to content

Commit 3f89b44

Browse files
committed
P2p: fix indefinite exchange of the same header list between peers when they are on different branches of a deep fork
1 parent 684ae7f commit 3f89b44

File tree

17 files changed

+935
-201
lines changed

17 files changed

+935
-201
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/
8181
- Fixed a potential indefinite stalling of a particular connection when both nodes start sending
8282
large amounts of data to each other.
8383

84+
- Fixed a potential indefinite exchange of the same header list requests and responses if
85+
the node and the peer are on different branches of a deep fork (such as the one happened
86+
due to the recent hard fork).
87+
8488
- Wallet CLI and RPC: the commands `account-utxos` and `standalone-multisig-utxos` and their RPC
8589
counterparts now return correct decimal amounts for tokens with non-default number of decimals.
8690

chainstate/src/detail/error_classification.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,10 @@ impl BlockProcessingErrorClassification for PropertyQueryError {
450450
| PropertyQueryError::InvalidStartingBlockHeightForMainchainBlocks(_)
451451
| PropertyQueryError::InvalidBlockHeightRange { .. }
452452
| PropertyQueryError::UnsupportedTokenV0InOrder(_)
453-
| PropertyQueryError::TokenInfoMissing(_) => BlockProcessingErrorClass::General,
453+
| PropertyQueryError::TokenInfoMissing(_)
454+
| PropertyQueryError::InvariantErrorLocatorDistancesWrongOrder => {
455+
BlockProcessingErrorClass::General
456+
}
454457
// Note: these errors are strange - sometimes they don't look like General, judging
455458
// by the code that uses them. But other times some of them seem to just wrap storage
456459
// errors.

chainstate/src/detail/query.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat
175175
}
176176
}
177177

178-
pub fn get_locator(&self) -> Result<Locator, PropertyQueryError> {
178+
pub fn get_locator_from_best_block(&self) -> Result<Locator, PropertyQueryError> {
179179
let best_block_index = self.chainstate_ref.get_best_block_index()?;
180180
let height = best_block_index.block_height();
181181
self.get_locator_from_height(height)
@@ -185,11 +185,48 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat
185185
&self,
186186
height: BlockHeight,
187187
) -> Result<Locator, PropertyQueryError> {
188-
let headers = locator_tip_distances()
188+
let block_id_results = locator_tip_distances()
189189
.map_while(|dist| height - dist)
190190
.map(|ht| self.chainstate_ref.get_block_id_by_height(ht));
191191

192-
itertools::process_results(headers, |iter| iter.flatten().collect::<Vec<_>>())
192+
itertools::process_results(block_id_results, |iter| iter.flatten().collect::<Vec<_>>())
193+
.map(Locator::new)
194+
}
195+
196+
pub fn get_locator_from_block_id(
197+
&self,
198+
block_id: &Id<Block>,
199+
) -> Result<Locator, PropertyQueryError> {
200+
let mut last_child_block_index =
201+
self.chainstate_ref.get_existing_gen_block_index(block_id.into())?;
202+
let mut already_on_mainchain = false;
203+
let starting_height = last_child_block_index.block_height();
204+
205+
let block_id_results = locator_tip_distances()
206+
.map_while(|dist| starting_height - dist)
207+
.map(|height| -> Result<_, PropertyQueryError> {
208+
if !already_on_mainchain {
209+
let last_child_block_id_from_height = self
210+
.chainstate_ref
211+
.get_block_id_by_height(last_child_block_index.block_height())?;
212+
already_on_mainchain =
213+
last_child_block_id_from_height == Some(last_child_block_index.block_id());
214+
}
215+
216+
let block_id = if already_on_mainchain {
217+
self.chainstate_ref
218+
.get_block_id_by_height(height)?
219+
.ok_or(PropertyQueryError::InvariantErrorLocatorDistancesWrongOrder)?
220+
} else {
221+
last_child_block_index =
222+
self.chainstate_ref.get_ancestor(&last_child_block_index, height)?;
223+
last_child_block_index.block_id()
224+
};
225+
226+
Ok(block_id)
227+
});
228+
229+
itertools::process_results(block_id_results, |iter| iter.collect::<Vec<_>>())
193230
.map(Locator::new)
194231
}
195232

chainstate/src/interface/chainstate_interface.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,14 @@ pub trait ChainstateInterface: Send + Sync {
9797
///
9898
/// This returns a relatively short sequence even for a long chain. Such sequence can be used
9999
/// to quickly find a common ancestor between different chains.
100-
fn get_locator(&self) -> Result<Locator, ChainstateError>;
100+
fn get_locator_from_best_block(&self) -> Result<Locator, ChainstateError>;
101101

102-
/// Returns a locator starting from the specified height.
102+
/// Returns a locator starting from the specified height on the main chain.
103103
fn get_locator_from_height(&self, height: BlockHeight) -> Result<Locator, ChainstateError>;
104104

105+
/// Returns a locator starting from the specified block (not necessarily on the main chain).
106+
fn get_locator_from_block_id(&self, block_id: &Id<Block>) -> Result<Locator, ChainstateError>;
107+
105108
/// Returns mainchain block ids with heights in the range start_height..end_height using
106109
/// the given step;
107110
fn get_block_ids_as_checkpoints(

chainstate/src/interface/chainstate_interface_impl.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,11 @@ where
232232
}
233233

234234
#[tracing::instrument(skip_all)]
235-
fn get_locator(&self) -> Result<Locator, ChainstateError> {
235+
fn get_locator_from_best_block(&self) -> Result<Locator, ChainstateError> {
236236
self.chainstate
237237
.query()
238238
.map_err(ChainstateError::from)?
239-
.get_locator()
239+
.get_locator_from_best_block()
240240
.map_err(ChainstateError::FailedToReadProperty)
241241
}
242242

@@ -249,6 +249,15 @@ where
249249
.map_err(ChainstateError::FailedToReadProperty)
250250
}
251251

252+
#[tracing::instrument(skip_all, fields(block_id = %block_id))]
253+
fn get_locator_from_block_id(&self, block_id: &Id<Block>) -> Result<Locator, ChainstateError> {
254+
self.chainstate
255+
.query()
256+
.map_err(ChainstateError::from)?
257+
.get_locator_from_block_id(block_id)
258+
.map_err(ChainstateError::FailedToReadProperty)
259+
}
260+
252261
#[tracing::instrument(skip(self))]
253262
fn get_block_ids_as_checkpoints(
254263
&self,

chainstate/src/interface/chainstate_interface_impl_delegation.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,18 @@ where
130130
self.deref().get_mainchain_blocks(from, max_count)
131131
}
132132

133-
fn get_locator(&self) -> Result<Locator, ChainstateError> {
134-
self.deref().get_locator()
133+
fn get_locator_from_best_block(&self) -> Result<Locator, ChainstateError> {
134+
self.deref().get_locator_from_best_block()
135135
}
136136

137137
fn get_locator_from_height(&self, height: BlockHeight) -> Result<Locator, ChainstateError> {
138138
self.deref().get_locator_from_height(height)
139139
}
140140

141+
fn get_locator_from_block_id(&self, block_id: &Id<Block>) -> Result<Locator, ChainstateError> {
142+
self.deref().get_locator_from_block_id(block_id)
143+
}
144+
141145
fn get_block_ids_as_checkpoints(
142146
&self,
143147
start_height: BlockHeight,

chainstate/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub use crate::{
4646
TransactionVerifierStorageError, MEDIAN_TIME_SPAN,
4747
},
4848
};
49-
pub use chainstate_types::{BlockIndex, GenBlockIndex, PropertyQueryError};
49+
pub use chainstate_types::{BlockIndex, GenBlockIndex, GenBlockIndexRef, PropertyQueryError};
5050
pub use constraints_value_accumulator;
5151
pub use detail::tx_verification_strategy::*;
5252
pub use interface::{chainstate_interface, chainstate_interface_impl_delegation};

chainstate/test-suite/src/tests/syncing_tests.rs

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,15 @@ fn process_a_trivial_block(#[case] seed: Seed) {
5858
#[rstest]
5959
#[trace]
6060
#[case(Seed::from_entropy())]
61-
fn get_locator(#[case] seed: Seed) {
61+
fn get_locator_from_best_block(#[case] seed: Seed) {
6262
utils::concurrency::model(move || {
6363
let mut rng = make_seedable_rng(seed);
6464
let mut btf = TestFramework::builder(&mut rng)
6565
// With the heavy checks enabled, this test takes several minutes to complete in debug builds.
6666
.with_chainstate_config(ChainstateConfig::new().with_heavy_checks_enabled(false))
6767
.build();
6868

69-
let locator = btf.chainstate.get_locator().unwrap();
69+
let locator = btf.chainstate.get_locator_from_best_block().unwrap();
7070
assert_eq!(locator.len(), 1);
7171
assert_eq!(&locator[0], &btf.genesis().get_id());
7272

@@ -79,7 +79,7 @@ fn get_locator(#[case] seed: Seed) {
7979
blocks += new_blocks;
8080

8181
// Check the locator length.
82-
let locator = btf.chainstate.get_locator().unwrap();
82+
let locator = btf.chainstate.get_locator_from_best_block().unwrap();
8383
assert_eq!(
8484
locator.len(),
8585
blocks.next_power_of_two().trailing_zeros() as usize + 1
@@ -95,6 +95,12 @@ fn get_locator(#[case] seed: Seed) {
9595
btf.chainstate.get_block_id_from_height(idx.unwrap()).unwrap().unwrap();
9696
assert_eq!(&expected, header);
9797
}
98+
99+
// Check that get_locator_from_block_id returns the same locator.
100+
let best_block_id = btf.to_chain_block_id(&btf.chainstate.get_best_block_id().unwrap());
101+
let locator_from_block_id =
102+
btf.chainstate.get_locator_from_block_id(&best_block_id).unwrap();
103+
assert_eq!(locator, locator_from_block_id);
98104
}
99105
});
100106
}
@@ -134,6 +140,88 @@ fn get_locator_from_height(#[case] seed: Seed) {
134140
btf.chainstate.get_block_id_from_height(idx.unwrap()).unwrap().unwrap();
135141
assert_eq!(&expected, header);
136142
}
143+
144+
// Check that get_locator_from_block_id returns the same locator.
145+
let block_id = btf.to_chain_block_id(
146+
&btf.chainstate.get_block_id_from_height(height.into()).unwrap().unwrap(),
147+
);
148+
let locator_from_block_id =
149+
btf.chainstate.get_locator_from_block_id(&block_id).unwrap();
150+
assert_eq!(locator, locator_from_block_id);
151+
}
152+
});
153+
}
154+
155+
#[rstest]
156+
#[trace]
157+
#[case(Seed::from_entropy())]
158+
fn get_locator_from_block_id(#[case] seed: Seed) {
159+
utils::concurrency::model(move || {
160+
let mut rng = make_seedable_rng(seed);
161+
162+
let mut tf1 = TestFramework::builder(&mut rng)
163+
// Heavy checks are useless here and they increase test run time noticeably
164+
.with_chainstate_config(ChainstateConfig::new().with_heavy_checks_enabled(false))
165+
.with_chain_config(
166+
chain::config::create_unit_test_config_builder()
167+
// Increase max_depth_for_reorg so that we can create a fork more than 1000
168+
// blocks deep.
169+
.max_depth_for_reorg(10_000.into())
170+
.build(),
171+
)
172+
.build();
173+
174+
let mainchain_len: usize = rng.gen_range(1000..2000);
175+
let stalechain_len = rng.gen_range(mainchain_len / 2..mainchain_len);
176+
let fork_height = rng.gen_range(0..stalechain_len);
177+
178+
let mainchain_block_ids = tf1
179+
.create_chain_return_ids(&tf1.genesis().get_id().into(), mainchain_len, &mut rng)
180+
.unwrap();
181+
let stale_branch_parent_id = if fork_height == 0 {
182+
tf1.genesis().get_id().into()
183+
} else {
184+
mainchain_block_ids[fork_height - 1]
185+
};
186+
187+
let stale_branch_block_ids = tf1
188+
.create_chain_return_ids(
189+
&stale_branch_parent_id,
190+
stalechain_len - fork_height,
191+
&mut rng,
192+
)
193+
.unwrap();
194+
195+
let whole_stalechain = mainchain_block_ids[0..fork_height]
196+
.iter()
197+
.chain(&stale_branch_block_ids[..])
198+
.map(|block_id| {
199+
tf1.chainstate.get_block(&tf1.to_chain_block_id(block_id)).unwrap().unwrap()
200+
});
201+
202+
let mut tf2 = TestFramework::builder(&mut rng)
203+
.with_chainstate_config(ChainstateConfig::new().with_heavy_checks_enabled(false))
204+
.build();
205+
206+
for block in whole_stalechain {
207+
tf2.chainstate.process_block(block, BlockSource::Local).unwrap();
208+
}
209+
210+
assert_eq!(tf1.best_block_id(), *mainchain_block_ids.last().unwrap());
211+
assert_eq!(tf2.best_block_id(), *stale_branch_block_ids.last().unwrap());
212+
213+
for _ in 0..8 {
214+
let stale_block_idx = rng.gen_range(0..stale_branch_block_ids.len());
215+
let height = (stale_block_idx + fork_height + 1) as u64;
216+
217+
let stale_block_id = tf1.to_chain_block_id(&stale_branch_block_ids[stale_block_idx]);
218+
219+
let locator_from_id =
220+
tf1.chainstate.get_locator_from_block_id(&stale_block_id).unwrap();
221+
let locator_from_height_in_tf2 =
222+
tf2.chainstate.get_locator_from_height(height.into()).unwrap();
223+
224+
assert_eq!(locator_from_id, locator_from_height_in_tf2);
137225
}
138226
});
139227
}
@@ -158,7 +246,7 @@ fn get_mainchain_headers_by_locator(#[case] seed: Seed) {
158246

159247
// The locator is from this exact chain, so get_mainchain_headers_by_locator
160248
// should return an empty sequence.
161-
let locator = tf.chainstate.get_locator().unwrap();
249+
let locator = tf.chainstate.get_locator_from_best_block().unwrap();
162250
assert_eq!(
163251
tf.chainstate.get_mainchain_headers_by_locator(&locator, header_limit).unwrap(),
164252
vec![],
@@ -211,11 +299,11 @@ fn get_headers_genesis(#[case] seed: Seed) {
211299
let genesis_id: Id<GenBlock> = btf.genesis().get_id().into();
212300

213301
btf.create_chain(&genesis_id, rng.gen_range(64..128), &mut rng).unwrap();
214-
let locator_1 = btf.chainstate.get_locator().unwrap();
302+
let locator_1 = btf.chainstate.get_locator_from_best_block().unwrap();
215303

216304
let chain_length = rng.gen_range(1200..2000);
217305
btf.create_chain(&genesis_id, chain_length, &mut rng).unwrap();
218-
let locator_2 = btf.chainstate.get_locator().unwrap();
306+
let locator_2 = btf.chainstate.get_locator_from_best_block().unwrap();
219307
assert_ne!(locator_1, locator_2);
220308
assert!(locator_1.len() < locator_2.len());
221309

@@ -254,7 +342,7 @@ fn get_headers_branching_chains(#[case] seed: Seed) {
254342
tf.create_chain(&tf.genesis().get_id().into(), common_height, &mut rng).unwrap();
255343

256344
tf.create_chain(&common_block_id, rng.gen_range(100..250), &mut rng).unwrap();
257-
let locator = tf.chainstate.get_locator().unwrap();
345+
let locator = tf.chainstate.get_locator_from_best_block().unwrap();
258346
tf.create_chain(&common_block_id, rng.gen_range(250..500), &mut rng).unwrap();
259347

260348
let headers = tf.chainstate.get_mainchain_headers_by_locator(&locator, 2000).unwrap();
@@ -540,15 +628,15 @@ fn get_headers_different_chains(#[case] seed: Seed) {
540628
tf2.create_chain(&prev_id, rng.gen_range(256..512), &mut rng).unwrap();
541629

542630
let header_count_limit = rng.gen_range(1000..3000);
543-
let locator = tf1.chainstate.get_locator().unwrap();
631+
let locator = tf1.chainstate.get_locator_from_best_block().unwrap();
544632
let headers = tf2
545633
.chainstate
546634
.get_mainchain_headers_by_locator(&locator, header_count_limit)
547635
.unwrap();
548636
let id = *headers[0].prev_block_id();
549637
tf1.gen_block_index(&id); // This panics if the ID is not found
550638

551-
let locator = tf2.chainstate.get_locator().unwrap();
639+
let locator = tf2.chainstate.get_locator_from_best_block().unwrap();
552640
let headers = tf1
553641
.chainstate
554642
.get_mainchain_headers_by_locator(&locator, header_count_limit)
@@ -898,7 +986,7 @@ fn get_block_ids_as_checkpoints(#[case] seed: Seed) {
898986
let mut rng = make_seedable_rng(seed);
899987
let mut btf = TestFramework::builder(&mut rng).build();
900988

901-
let locator = btf.chainstate.get_locator().unwrap();
989+
let locator = btf.chainstate.get_locator_from_best_block().unwrap();
902990
assert_eq!(locator.len(), 1);
903991
assert_eq!(&locator[0], &btf.genesis().get_id());
904992

chainstate/types/src/error.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub enum PropertyQueryError {
2626
StorageError(#[from] crate::storage_result::Error),
2727
#[error("Best block index not found")]
2828
BestBlockIndexNotFound,
29-
#[error("Block not found {0}")]
29+
#[error("Block not found: {0}")]
3030
BlockNotFound(Id<Block>),
3131
#[error("Block index not found for block {0}")]
3232
BlockIndexNotFound(Id<GenBlock>),
@@ -39,7 +39,7 @@ pub enum PropertyQueryError {
3939
},
4040
#[error("Block for height {0} not found")]
4141
BlockForHeightNotFound(BlockHeight),
42-
#[error("Error obtaining ancestor")]
42+
#[error("Error obtaining ancestor: {0}")]
4343
GetAncestorError(#[from] GetAncestorError),
4444
#[error("Genesis block has no header")]
4545
GenesisHeaderRequested,
@@ -60,6 +60,8 @@ pub enum PropertyQueryError {
6060
},
6161
#[error("Token info missing for token {0:x}")]
6262
TokenInfoMissing(TokenId),
63+
#[error("Locator distances have wrong order")]
64+
InvariantErrorLocatorDistancesWrongOrder,
6365
}
6466

6567
#[derive(Error, Debug, PartialEq, Eq, Clone)]

mocks/src/chainstate.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ mockall::mock! {
7676
max_count: usize,
7777
) -> Result<Vec<Block>, ChainstateError>;
7878
fn get_block_header(&self, block_id: &Id<Block>) -> Result<Option<SignedBlockHeader>, ChainstateError>;
79-
fn get_locator(&self) -> Result<Locator, ChainstateError>;
79+
fn get_locator_from_best_block(&self) -> Result<Locator, ChainstateError>;
8080
fn get_locator_from_height(&self, height: BlockHeight) -> Result<Locator, ChainstateError>;
81+
fn get_locator_from_block_id(&self, block_id: &Id<Block>) -> Result<Locator, ChainstateError>;
8182
fn get_block_ids_as_checkpoints(
8283
&self,
8384
start_height: BlockHeight,

0 commit comments

Comments
 (0)