mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-27 02:17:58 +00:00
Extract warp sync strategy from ChainSync (#2467)
Extract `WarpSync` (and `StateSync` as part of warp sync) from `ChainSync` as independent syncing strategy called by `SyncingEngine`. Introduce `SyncingStrategy` enum as a proxy between `SyncingEngine` and specific syncing strategies. ## Limitations Gap sync is kept in `ChainSync` for now because it shares the same set of peers as block syncing implementation in `ChainSync`. Extraction of a common context responsible for peer management in syncing strategies able to run in parallel is planned for a follow-up PR. ## Further improvements A possibility of conversion of `SyncingStartegy` into a trait should be evaluated. The main stopper for this is that different strategies need to communicate different actions to `SyncingEngine` and respond to different events / provide different APIs (e.g., requesting justifications is only possible via `ChainSync` and not through `WarpSync`; `SendWarpProofRequest` action is only relevant to `WarpSync`, etc.) --------- Co-authored-by: Aaro Altonen <48052676+altonen@users.noreply.github.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,967 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Tests of [`ChainSync`].
|
||||
|
||||
use super::*;
|
||||
use futures::executor::block_on;
|
||||
use sc_block_builder::BlockBuilderBuilder;
|
||||
use sc_network_common::sync::message::{BlockAnnounce, BlockData, BlockState, FromBlock};
|
||||
use sp_blockchain::HeaderBackend;
|
||||
use substrate_test_runtime_client::{
|
||||
runtime::{Block, Hash, Header},
|
||||
BlockBuilderExt, ClientBlockImportExt, ClientExt, DefaultTestClientBuilderExt, TestClient,
|
||||
TestClientBuilder, TestClientBuilderExt,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn processes_empty_response_on_justification_request_for_unknown_block() {
|
||||
// if we ask for a justification for a given block to a peer that doesn't know that block
|
||||
// (different from not having a justification), the peer will reply with an empty response.
|
||||
// internally we should process the response as the justification not being available.
|
||||
|
||||
let client = Arc::new(TestClientBuilder::new().build());
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 1, 64, None).unwrap();
|
||||
|
||||
let (a1_hash, a1_number) = {
|
||||
let a1 = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
(a1.hash(), *a1.header.number())
|
||||
};
|
||||
|
||||
// add a new peer with the same best block
|
||||
sync.add_peer(peer_id, a1_hash, a1_number);
|
||||
|
||||
// and request a justification for the block
|
||||
sync.request_justification(&a1_hash, a1_number);
|
||||
|
||||
// the justification request should be scheduled to that peer
|
||||
assert!(sync
|
||||
.justification_requests()
|
||||
.iter()
|
||||
.any(|(who, request)| { *who == peer_id && request.from == FromBlock::Hash(a1_hash) }));
|
||||
|
||||
// there are no extra pending requests
|
||||
assert_eq!(sync.extra_justifications.pending_requests().count(), 0);
|
||||
|
||||
// there's one in-flight extra request to the expected peer
|
||||
assert!(sync.extra_justifications.active_requests().any(|(who, (hash, number))| {
|
||||
*who == peer_id && *hash == a1_hash && *number == a1_number
|
||||
}));
|
||||
|
||||
// if the peer replies with an empty response (i.e. it doesn't know the block),
|
||||
// the active request should be cleared.
|
||||
sync.on_block_justification(peer_id, BlockResponse::<Block> { id: 0, blocks: vec![] })
|
||||
.unwrap();
|
||||
|
||||
// there should be no in-flight requests
|
||||
assert_eq!(sync.extra_justifications.active_requests().count(), 0);
|
||||
|
||||
// and the request should now be pending again, waiting for reschedule
|
||||
assert!(sync
|
||||
.extra_justifications
|
||||
.pending_requests()
|
||||
.any(|(hash, number)| { *hash == a1_hash && *number == a1_number }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_doesnt_affect_peers_downloading_finality_data() {
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
|
||||
let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 1, 64, None).unwrap();
|
||||
|
||||
let peer_id1 = PeerId::random();
|
||||
let peer_id2 = PeerId::random();
|
||||
let peer_id3 = PeerId::random();
|
||||
|
||||
let mut new_blocks = |n| {
|
||||
for _ in 0..n {
|
||||
let block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
block_on(client.import(BlockOrigin::Own, block.clone())).unwrap();
|
||||
}
|
||||
|
||||
let info = client.info();
|
||||
(info.best_hash, info.best_number)
|
||||
};
|
||||
|
||||
let (b1_hash, b1_number) = new_blocks(50);
|
||||
|
||||
// add 2 peers at blocks that we don't have locally
|
||||
sync.add_peer(peer_id1, Hash::random(), 42);
|
||||
sync.add_peer(peer_id2, Hash::random(), 10);
|
||||
|
||||
// we wil send block requests to these peers
|
||||
// for these blocks we don't know about
|
||||
assert!(sync
|
||||
.block_requests()
|
||||
.into_iter()
|
||||
.all(|(p, _)| { p == peer_id1 || p == peer_id2 }));
|
||||
|
||||
// add a new peer at a known block
|
||||
sync.add_peer(peer_id3, b1_hash, b1_number);
|
||||
|
||||
// we request a justification for a block we have locally
|
||||
sync.request_justification(&b1_hash, b1_number);
|
||||
|
||||
// the justification request should be scheduled to the
|
||||
// new peer which is at the given block
|
||||
assert!(sync.justification_requests().iter().any(|(p, r)| {
|
||||
*p == peer_id3 &&
|
||||
r.fields == BlockAttributes::JUSTIFICATION &&
|
||||
r.from == FromBlock::Hash(b1_hash)
|
||||
}));
|
||||
|
||||
assert_eq!(
|
||||
sync.peers.get(&peer_id3).unwrap().state,
|
||||
PeerSyncState::DownloadingJustification(b1_hash),
|
||||
);
|
||||
|
||||
// clear old actions
|
||||
let _ = sync.take_actions();
|
||||
|
||||
// we restart the sync state
|
||||
sync.restart();
|
||||
let actions = sync.take_actions().collect::<Vec<_>>();
|
||||
|
||||
// which should make us send out block requests to the first two peers
|
||||
assert_eq!(actions.len(), 2);
|
||||
assert!(actions.iter().all(|action| match action {
|
||||
ChainSyncAction::SendBlockRequest { peer_id, .. } =>
|
||||
peer_id == &peer_id1 || peer_id == &peer_id2,
|
||||
_ => false,
|
||||
}));
|
||||
|
||||
// peer 3 should be unaffected it was downloading finality data
|
||||
assert_eq!(
|
||||
sync.peers.get(&peer_id3).unwrap().state,
|
||||
PeerSyncState::DownloadingJustification(b1_hash),
|
||||
);
|
||||
|
||||
// Set common block to something that we don't have (e.g. failed import)
|
||||
sync.peers.get_mut(&peer_id3).unwrap().common_number = 100;
|
||||
sync.restart();
|
||||
assert_eq!(sync.peers.get(&peer_id3).unwrap().common_number, 50);
|
||||
}
|
||||
|
||||
/// Send a block annoucnement for the given `header`.
|
||||
fn send_block_announce(header: Header, peer_id: PeerId, sync: &mut ChainSync<Block, TestClient>) {
|
||||
let announce = BlockAnnounce {
|
||||
header: header.clone(),
|
||||
state: Some(BlockState::Best),
|
||||
data: Some(Vec::new()),
|
||||
};
|
||||
|
||||
let _ = sync.on_validated_block_announce(true, peer_id, &announce);
|
||||
}
|
||||
|
||||
/// Create a block response from the given `blocks`.
|
||||
fn create_block_response(blocks: Vec<Block>) -> BlockResponse<Block> {
|
||||
BlockResponse::<Block> {
|
||||
id: 0,
|
||||
blocks: blocks
|
||||
.into_iter()
|
||||
.map(|b| BlockData::<Block> {
|
||||
hash: b.hash(),
|
||||
header: Some(b.header().clone()),
|
||||
body: Some(b.deconstruct().1),
|
||||
indexed_body: None,
|
||||
receipt: None,
|
||||
message_queue: None,
|
||||
justification: None,
|
||||
justifications: None,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a block request from `sync` and check that is matches the expected request.
|
||||
fn get_block_request(
|
||||
sync: &mut ChainSync<Block, TestClient>,
|
||||
from: FromBlock<Hash, u64>,
|
||||
max: u32,
|
||||
peer: &PeerId,
|
||||
) -> BlockRequest<Block> {
|
||||
let requests = sync.block_requests();
|
||||
|
||||
log::trace!(target: LOG_TARGET, "Requests: {requests:?}");
|
||||
|
||||
assert_eq!(1, requests.len());
|
||||
assert_eq!(*peer, requests[0].0);
|
||||
|
||||
let request = requests[0].1.clone();
|
||||
|
||||
assert_eq!(from, request.from);
|
||||
assert_eq!(Some(max), request.max);
|
||||
request
|
||||
}
|
||||
|
||||
/// Build and import a new best block.
|
||||
fn build_block(client: &mut Arc<TestClient>, at: Option<Hash>, fork: bool) -> Block {
|
||||
let at = at.unwrap_or_else(|| client.info().best_hash);
|
||||
|
||||
let mut block_builder = BlockBuilderBuilder::new(&**client)
|
||||
.on_parent_block(at)
|
||||
.fetch_parent_block_number(&**client)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
if fork {
|
||||
block_builder.push_storage_change(vec![1, 2, 3], Some(vec![4, 5, 6])).unwrap();
|
||||
}
|
||||
|
||||
let block = block_builder.build().unwrap().block;
|
||||
|
||||
block_on(client.import(BlockOrigin::Own, block.clone())).unwrap();
|
||||
block
|
||||
}
|
||||
|
||||
fn unwrap_from_block_number(from: FromBlock<Hash, u64>) -> u64 {
|
||||
if let FromBlock::Number(from) = from {
|
||||
from
|
||||
} else {
|
||||
panic!("Expected a number!");
|
||||
}
|
||||
}
|
||||
|
||||
/// A regression test for a behavior we have seen on a live network.
|
||||
///
|
||||
/// The scenario is that the node is doing a full resync and is connected to some node that is
|
||||
/// doing a major sync as well. This other node that is doing a major sync will finish before
|
||||
/// our node and send a block announcement message, but we don't have seen any block
|
||||
/// announcement from this node in its sync process. Meaning our common number didn't change. It
|
||||
/// is now expected that we start an ancestor search to find the common number.
|
||||
#[test]
|
||||
fn do_ancestor_search_when_common_block_to_best_qeued_gap_is_to_big() {
|
||||
sp_tracing::try_init_simple();
|
||||
|
||||
let blocks = {
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
(0..MAX_DOWNLOAD_AHEAD * 2)
|
||||
.map(|_| build_block(&mut client, None, false))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
let info = client.info();
|
||||
|
||||
let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 5, 64, None).unwrap();
|
||||
|
||||
let peer_id1 = PeerId::random();
|
||||
let peer_id2 = PeerId::random();
|
||||
|
||||
let best_block = blocks.last().unwrap().clone();
|
||||
let max_blocks_to_request = sync.max_blocks_per_request;
|
||||
// Connect the node we will sync from
|
||||
sync.add_peer(peer_id1, best_block.hash(), *best_block.header().number());
|
||||
sync.add_peer(peer_id2, info.best_hash, 0);
|
||||
|
||||
let mut best_block_num = 0;
|
||||
while best_block_num < MAX_DOWNLOAD_AHEAD {
|
||||
let request = get_block_request(
|
||||
&mut sync,
|
||||
FromBlock::Number(max_blocks_to_request as u64 + best_block_num as u64),
|
||||
max_blocks_to_request as u32,
|
||||
&peer_id1,
|
||||
);
|
||||
|
||||
let from = unwrap_from_block_number(request.from.clone());
|
||||
|
||||
let mut resp_blocks = blocks[best_block_num as usize..from as usize].to_vec();
|
||||
resp_blocks.reverse();
|
||||
|
||||
let response = create_block_response(resp_blocks.clone());
|
||||
|
||||
// Clear old actions to not deal with them
|
||||
let _ = sync.take_actions();
|
||||
|
||||
sync.on_block_data(&peer_id1, Some(request), response).unwrap();
|
||||
|
||||
let actions = sync.take_actions().collect::<Vec<_>>();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert!(matches!(
|
||||
&actions[0],
|
||||
ChainSyncAction::ImportBlocks{ origin: _, blocks } if blocks.len() == max_blocks_to_request as usize,
|
||||
));
|
||||
|
||||
best_block_num += max_blocks_to_request as u32;
|
||||
|
||||
let _ = sync.on_blocks_processed(
|
||||
max_blocks_to_request as usize,
|
||||
max_blocks_to_request as usize,
|
||||
resp_blocks
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|b| {
|
||||
(
|
||||
Ok(BlockImportStatus::ImportedUnknown(
|
||||
*b.header().number(),
|
||||
Default::default(),
|
||||
Some(peer_id1),
|
||||
)),
|
||||
b.hash(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
|
||||
resp_blocks
|
||||
.into_iter()
|
||||
.rev()
|
||||
.for_each(|b| block_on(client.import_as_final(BlockOrigin::Own, b)).unwrap());
|
||||
}
|
||||
|
||||
// "Wait" for the queue to clear
|
||||
sync.queue_blocks.clear();
|
||||
|
||||
// Let peer2 announce that it finished syncing
|
||||
send_block_announce(best_block.header().clone(), peer_id2, &mut sync);
|
||||
|
||||
let (peer1_req, peer2_req) =
|
||||
sync.block_requests().into_iter().fold((None, None), |res, req| {
|
||||
if req.0 == peer_id1 {
|
||||
(Some(req.1), res.1)
|
||||
} else if req.0 == peer_id2 {
|
||||
(res.0, Some(req.1))
|
||||
} else {
|
||||
panic!("Unexpected req: {:?}", req)
|
||||
}
|
||||
});
|
||||
|
||||
// We should now do an ancestor search to find the correct common block.
|
||||
let peer2_req = peer2_req.unwrap();
|
||||
assert_eq!(Some(1), peer2_req.max);
|
||||
assert_eq!(FromBlock::Number(best_block_num as u64), peer2_req.from);
|
||||
|
||||
let response = create_block_response(vec![blocks[(best_block_num - 1) as usize].clone()]);
|
||||
|
||||
// Clear old actions to not deal with them
|
||||
let _ = sync.take_actions();
|
||||
|
||||
sync.on_block_data(&peer_id2, Some(peer2_req), response).unwrap();
|
||||
|
||||
let actions = sync.take_actions().collect::<Vec<_>>();
|
||||
assert!(actions.is_empty());
|
||||
|
||||
let peer1_from = unwrap_from_block_number(peer1_req.unwrap().from);
|
||||
|
||||
// As we are on the same chain, we should directly continue with requesting blocks from
|
||||
// peer 2 as well.
|
||||
get_block_request(
|
||||
&mut sync,
|
||||
FromBlock::Number(peer1_from + max_blocks_to_request as u64),
|
||||
max_blocks_to_request as u32,
|
||||
&peer_id2,
|
||||
);
|
||||
}
|
||||
|
||||
/// A test that ensures that we can sync a huge fork.
|
||||
///
|
||||
/// The following scenario:
|
||||
/// A peer connects to us and we both have the common block 512. The last finalized is 2048.
|
||||
/// Our best block is 4096. The peer send us a block announcement with 4097 from a fork.
|
||||
///
|
||||
/// We will first do an ancestor search to find the common block. After that we start to sync
|
||||
/// the fork and finish it ;)
|
||||
#[test]
|
||||
fn can_sync_huge_fork() {
|
||||
sp_tracing::try_init_simple();
|
||||
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
let blocks = (0..MAX_BLOCKS_TO_LOOK_BACKWARDS * 4)
|
||||
.map(|_| build_block(&mut client, None, false))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let fork_blocks = {
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
let fork_blocks = blocks[..MAX_BLOCKS_TO_LOOK_BACKWARDS as usize * 2]
|
||||
.into_iter()
|
||||
.inspect(|b| block_on(client.import(BlockOrigin::Own, (*b).clone())).unwrap())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
fork_blocks
|
||||
.into_iter()
|
||||
.chain(
|
||||
(0..MAX_BLOCKS_TO_LOOK_BACKWARDS * 2 + 1)
|
||||
.map(|_| build_block(&mut client, None, true)),
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let info = client.info();
|
||||
|
||||
let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 5, 64, None).unwrap();
|
||||
|
||||
let finalized_block = blocks[MAX_BLOCKS_TO_LOOK_BACKWARDS as usize * 2 - 1].clone();
|
||||
let just = (*b"TEST", Vec::new());
|
||||
client.finalize_block(finalized_block.hash(), Some(just)).unwrap();
|
||||
sync.update_chain_info(&info.best_hash, info.best_number);
|
||||
|
||||
let peer_id1 = PeerId::random();
|
||||
|
||||
let common_block = blocks[MAX_BLOCKS_TO_LOOK_BACKWARDS as usize / 2].clone();
|
||||
// Connect the node we will sync from
|
||||
sync.add_peer(peer_id1, common_block.hash(), *common_block.header().number());
|
||||
|
||||
send_block_announce(fork_blocks.last().unwrap().header().clone(), peer_id1, &mut sync);
|
||||
|
||||
let mut request =
|
||||
get_block_request(&mut sync, FromBlock::Number(info.best_number), 1, &peer_id1);
|
||||
|
||||
// Discard old actions we are not interested in
|
||||
let _ = sync.take_actions();
|
||||
|
||||
// Do the ancestor search
|
||||
loop {
|
||||
let block = &fork_blocks[unwrap_from_block_number(request.from.clone()) as usize - 1];
|
||||
let response = create_block_response(vec![block.clone()]);
|
||||
|
||||
sync.on_block_data(&peer_id1, Some(request), response).unwrap();
|
||||
|
||||
let actions = sync.take_actions().collect::<Vec<_>>();
|
||||
|
||||
request = if actions.is_empty() {
|
||||
// We found the ancenstor
|
||||
break
|
||||
} else {
|
||||
assert_eq!(actions.len(), 1);
|
||||
match &actions[0] {
|
||||
ChainSyncAction::SendBlockRequest { peer_id: _, request } => request.clone(),
|
||||
action @ _ => panic!("Unexpected action: {action:?}"),
|
||||
}
|
||||
};
|
||||
|
||||
log::trace!(target: LOG_TARGET, "Request: {request:?}");
|
||||
}
|
||||
|
||||
// Now request and import the fork.
|
||||
let mut best_block_num = *finalized_block.header().number() as u32;
|
||||
let max_blocks_to_request = sync.max_blocks_per_request;
|
||||
while best_block_num < *fork_blocks.last().unwrap().header().number() as u32 - 1 {
|
||||
let request = get_block_request(
|
||||
&mut sync,
|
||||
FromBlock::Number(max_blocks_to_request as u64 + best_block_num as u64),
|
||||
max_blocks_to_request as u32,
|
||||
&peer_id1,
|
||||
);
|
||||
|
||||
let from = unwrap_from_block_number(request.from.clone());
|
||||
|
||||
let mut resp_blocks = fork_blocks[best_block_num as usize..from as usize].to_vec();
|
||||
resp_blocks.reverse();
|
||||
|
||||
let response = create_block_response(resp_blocks.clone());
|
||||
|
||||
sync.on_block_data(&peer_id1, Some(request), response).unwrap();
|
||||
|
||||
let actions = sync.take_actions().collect::<Vec<_>>();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert!(matches!(
|
||||
&actions[0],
|
||||
ChainSyncAction::ImportBlocks{ origin: _, blocks } if blocks.len() == sync.max_blocks_per_request as usize
|
||||
));
|
||||
|
||||
best_block_num += sync.max_blocks_per_request as u32;
|
||||
|
||||
sync.on_blocks_processed(
|
||||
max_blocks_to_request as usize,
|
||||
max_blocks_to_request as usize,
|
||||
resp_blocks
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|b| {
|
||||
(
|
||||
Ok(BlockImportStatus::ImportedUnknown(
|
||||
*b.header().number(),
|
||||
Default::default(),
|
||||
Some(peer_id1),
|
||||
)),
|
||||
b.hash(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
|
||||
// Discard pending actions
|
||||
let _ = sync.take_actions();
|
||||
|
||||
resp_blocks
|
||||
.into_iter()
|
||||
.rev()
|
||||
.for_each(|b| block_on(client.import(BlockOrigin::Own, b)).unwrap());
|
||||
}
|
||||
|
||||
// Request the tip
|
||||
get_block_request(&mut sync, FromBlock::Hash(fork_blocks.last().unwrap().hash()), 1, &peer_id1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syncs_fork_without_duplicate_requests() {
|
||||
sp_tracing::try_init_simple();
|
||||
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
let blocks = (0..MAX_BLOCKS_TO_LOOK_BACKWARDS * 4)
|
||||
.map(|_| build_block(&mut client, None, false))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let fork_blocks = {
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
let fork_blocks = blocks[..MAX_BLOCKS_TO_LOOK_BACKWARDS as usize * 2]
|
||||
.into_iter()
|
||||
.inspect(|b| block_on(client.import(BlockOrigin::Own, (*b).clone())).unwrap())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
fork_blocks
|
||||
.into_iter()
|
||||
.chain(
|
||||
(0..MAX_BLOCKS_TO_LOOK_BACKWARDS * 2 + 1)
|
||||
.map(|_| build_block(&mut client, None, true)),
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let info = client.info();
|
||||
|
||||
let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 5, 64, None).unwrap();
|
||||
|
||||
let finalized_block = blocks[MAX_BLOCKS_TO_LOOK_BACKWARDS as usize * 2 - 1].clone();
|
||||
let just = (*b"TEST", Vec::new());
|
||||
client.finalize_block(finalized_block.hash(), Some(just)).unwrap();
|
||||
sync.update_chain_info(&info.best_hash, info.best_number);
|
||||
|
||||
let peer_id1 = PeerId::random();
|
||||
|
||||
let common_block = blocks[MAX_BLOCKS_TO_LOOK_BACKWARDS as usize / 2].clone();
|
||||
// Connect the node we will sync from
|
||||
sync.add_peer(peer_id1, common_block.hash(), *common_block.header().number());
|
||||
|
||||
send_block_announce(fork_blocks.last().unwrap().header().clone(), peer_id1, &mut sync);
|
||||
|
||||
let mut request =
|
||||
get_block_request(&mut sync, FromBlock::Number(info.best_number), 1, &peer_id1);
|
||||
|
||||
// Discard pending actions
|
||||
let _ = sync.take_actions();
|
||||
|
||||
// Do the ancestor search
|
||||
loop {
|
||||
let block = &fork_blocks[unwrap_from_block_number(request.from.clone()) as usize - 1];
|
||||
let response = create_block_response(vec![block.clone()]);
|
||||
|
||||
sync.on_block_data(&peer_id1, Some(request), response).unwrap();
|
||||
|
||||
let actions = sync.take_actions().collect::<Vec<_>>();
|
||||
|
||||
request = if actions.is_empty() {
|
||||
// We found the ancenstor
|
||||
break
|
||||
} else {
|
||||
assert_eq!(actions.len(), 1);
|
||||
match &actions[0] {
|
||||
ChainSyncAction::SendBlockRequest { peer_id: _, request } => request.clone(),
|
||||
action @ _ => panic!("Unexpected action: {action:?}"),
|
||||
}
|
||||
};
|
||||
|
||||
log::trace!(target: LOG_TARGET, "Request: {request:?}");
|
||||
}
|
||||
|
||||
// Now request and import the fork.
|
||||
let mut best_block_num = *finalized_block.header().number() as u32;
|
||||
let max_blocks_to_request = sync.max_blocks_per_request;
|
||||
|
||||
let mut request = get_block_request(
|
||||
&mut sync,
|
||||
FromBlock::Number(max_blocks_to_request as u64 + best_block_num as u64),
|
||||
max_blocks_to_request as u32,
|
||||
&peer_id1,
|
||||
);
|
||||
let last_block_num = *fork_blocks.last().unwrap().header().number() as u32 - 1;
|
||||
while best_block_num < last_block_num {
|
||||
let from = unwrap_from_block_number(request.from.clone());
|
||||
|
||||
let mut resp_blocks = fork_blocks[best_block_num as usize..from as usize].to_vec();
|
||||
resp_blocks.reverse();
|
||||
|
||||
let response = create_block_response(resp_blocks.clone());
|
||||
|
||||
// Discard old actions
|
||||
let _ = sync.take_actions();
|
||||
|
||||
sync.on_block_data(&peer_id1, Some(request.clone()), response).unwrap();
|
||||
|
||||
let actions = sync.take_actions().collect::<Vec<_>>();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert!(matches!(
|
||||
&actions[0],
|
||||
ChainSyncAction::ImportBlocks{ origin: _, blocks } if blocks.len() == max_blocks_to_request as usize
|
||||
));
|
||||
|
||||
best_block_num += max_blocks_to_request as u32;
|
||||
|
||||
if best_block_num < last_block_num {
|
||||
// make sure we're not getting a duplicate request in the time before the blocks are
|
||||
// processed
|
||||
request = get_block_request(
|
||||
&mut sync,
|
||||
FromBlock::Number(max_blocks_to_request as u64 + best_block_num as u64),
|
||||
max_blocks_to_request as u32,
|
||||
&peer_id1,
|
||||
);
|
||||
}
|
||||
|
||||
let mut notify_imported: Vec<_> = resp_blocks
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|b| {
|
||||
(
|
||||
Ok(BlockImportStatus::ImportedUnknown(
|
||||
*b.header().number(),
|
||||
Default::default(),
|
||||
Some(peer_id1),
|
||||
)),
|
||||
b.hash(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// The import queue may send notifications in batches of varying size. So we simulate
|
||||
// this here by splitting the batch into 2 notifications.
|
||||
let max_blocks_to_request = sync.max_blocks_per_request;
|
||||
let second_batch = notify_imported.split_off(notify_imported.len() / 2);
|
||||
let _ = sync.on_blocks_processed(
|
||||
max_blocks_to_request as usize,
|
||||
max_blocks_to_request as usize,
|
||||
notify_imported,
|
||||
);
|
||||
|
||||
let _ = sync.on_blocks_processed(
|
||||
max_blocks_to_request as usize,
|
||||
max_blocks_to_request as usize,
|
||||
second_batch,
|
||||
);
|
||||
|
||||
resp_blocks
|
||||
.into_iter()
|
||||
.rev()
|
||||
.for_each(|b| block_on(client.import(BlockOrigin::Own, b)).unwrap());
|
||||
}
|
||||
|
||||
// Request the tip
|
||||
get_block_request(&mut sync, FromBlock::Hash(fork_blocks.last().unwrap().hash()), 1, &peer_id1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removes_target_fork_on_disconnect() {
|
||||
sp_tracing::try_init_simple();
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
let blocks = (0..3).map(|_| build_block(&mut client, None, false)).collect::<Vec<_>>();
|
||||
|
||||
let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 1, 64, None).unwrap();
|
||||
|
||||
let peer_id1 = PeerId::random();
|
||||
let common_block = blocks[1].clone();
|
||||
// Connect the node we will sync from
|
||||
sync.add_peer(peer_id1, common_block.hash(), *common_block.header().number());
|
||||
|
||||
// Create a "new" header and announce it
|
||||
let mut header = blocks[0].header().clone();
|
||||
header.number = 4;
|
||||
send_block_announce(header, peer_id1, &mut sync);
|
||||
assert!(sync.fork_targets.len() == 1);
|
||||
|
||||
let _ = sync.remove_peer(&peer_id1);
|
||||
assert!(sync.fork_targets.len() == 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_import_response_with_missing_blocks() {
|
||||
sp_tracing::try_init_simple();
|
||||
let mut client2 = Arc::new(TestClientBuilder::new().build());
|
||||
let blocks = (0..4).map(|_| build_block(&mut client2, None, false)).collect::<Vec<_>>();
|
||||
|
||||
let empty_client = Arc::new(TestClientBuilder::new().build());
|
||||
|
||||
let mut sync = ChainSync::new(ChainSyncMode::Full, empty_client.clone(), 1, 64, None).unwrap();
|
||||
|
||||
let peer_id1 = PeerId::random();
|
||||
let best_block = blocks[3].clone();
|
||||
sync.add_peer(peer_id1, best_block.hash(), *best_block.header().number());
|
||||
|
||||
sync.peers.get_mut(&peer_id1).unwrap().state = PeerSyncState::Available;
|
||||
sync.peers.get_mut(&peer_id1).unwrap().common_number = 0;
|
||||
|
||||
// Request all missing blocks and respond only with some.
|
||||
let request = get_block_request(&mut sync, FromBlock::Hash(best_block.hash()), 4, &peer_id1);
|
||||
let response =
|
||||
create_block_response(vec![blocks[3].clone(), blocks[2].clone(), blocks[1].clone()]);
|
||||
sync.on_block_data(&peer_id1, Some(request.clone()), response).unwrap();
|
||||
assert_eq!(sync.best_queued_number, 0);
|
||||
|
||||
// Request should only contain the missing block.
|
||||
let request = get_block_request(&mut sync, FromBlock::Number(1), 1, &peer_id1);
|
||||
let response = create_block_response(vec![blocks[0].clone()]);
|
||||
sync.on_block_data(&peer_id1, Some(request), response).unwrap();
|
||||
assert_eq!(sync.best_queued_number, 4);
|
||||
}
|
||||
#[test]
|
||||
fn ancestor_search_repeat() {
|
||||
let state = AncestorSearchState::<Block>::BinarySearch(1, 3);
|
||||
assert!(handle_ancestor_search_state(&state, 2, true).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_restart_removes_block_but_not_justification_requests() {
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 1, 64, None).unwrap();
|
||||
|
||||
let peers = vec![PeerId::random(), PeerId::random()];
|
||||
|
||||
let mut new_blocks = |n| {
|
||||
for _ in 0..n {
|
||||
let block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
block_on(client.import(BlockOrigin::Own, block.clone())).unwrap();
|
||||
}
|
||||
|
||||
let info = client.info();
|
||||
(info.best_hash, info.best_number)
|
||||
};
|
||||
|
||||
let (b1_hash, b1_number) = new_blocks(50);
|
||||
|
||||
// add new peer and request blocks from them
|
||||
sync.add_peer(peers[0], Hash::random(), 42);
|
||||
|
||||
// we don't actually perform any requests, just keep track of peers waiting for a response
|
||||
let mut pending_responses = HashSet::new();
|
||||
|
||||
// we wil send block requests to these peers
|
||||
// for these blocks we don't know about
|
||||
for (peer, _request) in sync.block_requests() {
|
||||
// "send" request
|
||||
pending_responses.insert(peer);
|
||||
}
|
||||
|
||||
// add a new peer at a known block
|
||||
sync.add_peer(peers[1], b1_hash, b1_number);
|
||||
|
||||
// we request a justification for a block we have locally
|
||||
sync.request_justification(&b1_hash, b1_number);
|
||||
|
||||
// the justification request should be scheduled to the
|
||||
// new peer which is at the given block
|
||||
let mut requests = sync.justification_requests();
|
||||
assert_eq!(requests.len(), 1);
|
||||
let (peer, _request) = requests.remove(0);
|
||||
// "send" request
|
||||
assert!(pending_responses.insert(peer));
|
||||
|
||||
assert!(!std::matches!(
|
||||
sync.peers.get(&peers[0]).unwrap().state,
|
||||
PeerSyncState::DownloadingJustification(_),
|
||||
));
|
||||
assert_eq!(
|
||||
sync.peers.get(&peers[1]).unwrap().state,
|
||||
PeerSyncState::DownloadingJustification(b1_hash),
|
||||
);
|
||||
assert_eq!(pending_responses.len(), 2);
|
||||
|
||||
// discard old actions
|
||||
let _ = sync.take_actions();
|
||||
|
||||
// restart sync
|
||||
sync.restart();
|
||||
let actions = sync.take_actions().collect::<Vec<_>>();
|
||||
for action in actions.iter() {
|
||||
match action {
|
||||
ChainSyncAction::CancelBlockRequest { peer_id } => {
|
||||
pending_responses.remove(&peer_id);
|
||||
},
|
||||
ChainSyncAction::SendBlockRequest { peer_id, .. } => {
|
||||
// we drop obsolete response, but don't register a new request, it's checked in
|
||||
// the `assert!` below
|
||||
pending_responses.remove(&peer_id);
|
||||
},
|
||||
action @ _ => panic!("Unexpected action: {action:?}"),
|
||||
}
|
||||
}
|
||||
assert!(actions.iter().any(|action| {
|
||||
match action {
|
||||
ChainSyncAction::SendBlockRequest { peer_id, .. } => peer_id == &peers[0],
|
||||
_ => false,
|
||||
}
|
||||
}));
|
||||
|
||||
assert_eq!(pending_responses.len(), 1);
|
||||
assert!(pending_responses.contains(&peers[1]));
|
||||
assert_eq!(
|
||||
sync.peers.get(&peers[1]).unwrap().state,
|
||||
PeerSyncState::DownloadingJustification(b1_hash),
|
||||
);
|
||||
let _ = sync.remove_peer(&peers[1]);
|
||||
pending_responses.remove(&peers[1]);
|
||||
assert_eq!(pending_responses.len(), 0);
|
||||
}
|
||||
|
||||
/// The test demonstrates https://github.com/paritytech/polkadot-sdk/issues/2094.
|
||||
/// TODO: convert it into desired behavior test once the issue is fixed (see inline comments).
|
||||
/// The issue: we currently rely on block numbers instead of block hash
|
||||
/// to download blocks from peers. As a result, we can end up with blocks
|
||||
/// from different forks as shown by the test.
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn request_across_forks() {
|
||||
sp_tracing::try_init_simple();
|
||||
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
let blocks = (0..100).map(|_| build_block(&mut client, None, false)).collect::<Vec<_>>();
|
||||
|
||||
let fork_a_blocks = {
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
let mut fork_blocks = blocks[..]
|
||||
.into_iter()
|
||||
.inspect(|b| {
|
||||
assert!(matches!(client.block(*b.header.parent_hash()), Ok(Some(_))));
|
||||
block_on(client.import(BlockOrigin::Own, (*b).clone())).unwrap()
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for _ in 0..10 {
|
||||
fork_blocks.push(build_block(&mut client, None, false));
|
||||
}
|
||||
fork_blocks
|
||||
};
|
||||
|
||||
let fork_b_blocks = {
|
||||
let mut client = Arc::new(TestClientBuilder::new().build());
|
||||
let mut fork_blocks = blocks[..]
|
||||
.into_iter()
|
||||
.inspect(|b| {
|
||||
assert!(matches!(client.block(*b.header.parent_hash()), Ok(Some(_))));
|
||||
block_on(client.import(BlockOrigin::Own, (*b).clone())).unwrap()
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for _ in 0..10 {
|
||||
fork_blocks.push(build_block(&mut client, None, true));
|
||||
}
|
||||
fork_blocks
|
||||
};
|
||||
|
||||
let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 5, 64, None).unwrap();
|
||||
|
||||
// Add the peers, all at the common ancestor 100.
|
||||
let common_block = blocks.last().unwrap();
|
||||
let peer_id1 = PeerId::random();
|
||||
sync.add_peer(peer_id1, common_block.hash(), *common_block.header().number());
|
||||
let peer_id2 = PeerId::random();
|
||||
sync.add_peer(peer_id2, common_block.hash(), *common_block.header().number());
|
||||
|
||||
// Peer 1 announces 107 from fork 1, 100-107 get downloaded.
|
||||
{
|
||||
let block = (&fork_a_blocks[106]).clone();
|
||||
let peer = peer_id1;
|
||||
log::trace!(target: LOG_TARGET, "<1> {peer} announces from fork 1");
|
||||
send_block_announce(block.header().clone(), peer, &mut sync);
|
||||
let request = get_block_request(&mut sync, FromBlock::Hash(block.hash()), 7, &peer);
|
||||
let mut resp_blocks = fork_a_blocks[100_usize..107_usize].to_vec();
|
||||
resp_blocks.reverse();
|
||||
let response = create_block_response(resp_blocks.clone());
|
||||
|
||||
// Drop old actions
|
||||
let _ = sync.take_actions();
|
||||
|
||||
sync.on_block_data(&peer, Some(request), response).unwrap();
|
||||
let actions = sync.take_actions().collect::<Vec<_>>();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert!(matches!(
|
||||
&actions[0],
|
||||
ChainSyncAction::ImportBlocks{ origin: _, blocks } if blocks.len() == 7_usize
|
||||
));
|
||||
assert_eq!(sync.best_queued_number, 107);
|
||||
assert_eq!(sync.best_queued_hash, block.hash());
|
||||
assert!(sync.is_known(&block.header.parent_hash()));
|
||||
}
|
||||
|
||||
// Peer 2 also announces 107 from fork 1.
|
||||
{
|
||||
let prev_best_number = sync.best_queued_number;
|
||||
let prev_best_hash = sync.best_queued_hash;
|
||||
let peer = peer_id2;
|
||||
log::trace!(target: LOG_TARGET, "<2> {peer} announces from fork 1");
|
||||
for i in 100..107 {
|
||||
let block = (&fork_a_blocks[i]).clone();
|
||||
send_block_announce(block.header().clone(), peer, &mut sync);
|
||||
assert!(sync.block_requests().is_empty());
|
||||
}
|
||||
assert_eq!(sync.best_queued_number, prev_best_number);
|
||||
assert_eq!(sync.best_queued_hash, prev_best_hash);
|
||||
}
|
||||
|
||||
// Peer 2 undergoes reorg, announces 108 from fork 2, gets downloaded even though we
|
||||
// don't have the parent from fork 2.
|
||||
{
|
||||
let block = (&fork_b_blocks[107]).clone();
|
||||
let peer = peer_id2;
|
||||
log::trace!(target: LOG_TARGET, "<3> {peer} announces from fork 2");
|
||||
send_block_announce(block.header().clone(), peer, &mut sync);
|
||||
// TODO: when the issue is fixed, this test can be changed to test the
|
||||
// expected behavior instead. The needed changes would be:
|
||||
// 1. Remove the `#[should_panic]` directive
|
||||
// 2. These should be changed to check that sync.block_requests().is_empty(), after the
|
||||
// block is announced.
|
||||
let request = get_block_request(&mut sync, FromBlock::Hash(block.hash()), 1, &peer);
|
||||
let response = create_block_response(vec![block.clone()]);
|
||||
|
||||
// Drop old actions we are not going to check
|
||||
let _ = sync.take_actions();
|
||||
|
||||
sync.on_block_data(&peer, Some(request), response).unwrap();
|
||||
let actions = sync.take_actions().collect::<Vec<_>>();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert!(matches!(
|
||||
&actions[0],
|
||||
ChainSyncAction::ImportBlocks{ origin: _, blocks } if blocks.len() == 1_usize
|
||||
));
|
||||
assert!(sync.is_known(&block.header.parent_hash()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,754 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! State sync strategy.
|
||||
|
||||
use crate::{
|
||||
schema::v1::StateResponse,
|
||||
strategy::state_sync::{ImportResult, StateSync, StateSyncProvider},
|
||||
types::{BadPeer, OpaqueStateRequest, OpaqueStateResponse, SyncState, SyncStatus},
|
||||
LOG_TARGET,
|
||||
};
|
||||
use libp2p::PeerId;
|
||||
use log::{debug, error, trace};
|
||||
use sc_client_api::ProofProvider;
|
||||
use sc_consensus::{BlockImportError, BlockImportStatus, IncomingBlock};
|
||||
use sc_network_common::sync::message::BlockAnnounce;
|
||||
use sp_consensus::BlockOrigin;
|
||||
use sp_runtime::{
|
||||
traits::{Block as BlockT, Header, NumberFor},
|
||||
Justifications, SaturatedConversion,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
mod rep {
|
||||
use sc_network::ReputationChange as Rep;
|
||||
|
||||
/// Peer response data does not have requested bits.
|
||||
pub const BAD_RESPONSE: Rep = Rep::new(-(1 << 12), "Incomplete response");
|
||||
|
||||
/// Reputation change for peers which send us a known bad state.
|
||||
pub const BAD_STATE: Rep = Rep::new(-(1 << 29), "Bad state");
|
||||
}
|
||||
|
||||
/// Action that should be performed on [`StateStrategy`]'s behalf.
|
||||
pub enum StateStrategyAction<B: BlockT> {
|
||||
/// Send state request to peer.
|
||||
SendStateRequest { peer_id: PeerId, request: OpaqueStateRequest },
|
||||
/// Disconnect and report peer.
|
||||
DropPeer(BadPeer),
|
||||
/// Import blocks.
|
||||
ImportBlocks { origin: BlockOrigin, blocks: Vec<IncomingBlock<B>> },
|
||||
/// State sync has finished.
|
||||
Finished,
|
||||
}
|
||||
|
||||
enum PeerState {
|
||||
Available,
|
||||
DownloadingState,
|
||||
}
|
||||
|
||||
impl PeerState {
|
||||
fn is_available(&self) -> bool {
|
||||
matches!(self, PeerState::Available)
|
||||
}
|
||||
}
|
||||
|
||||
struct Peer<B: BlockT> {
|
||||
best_number: NumberFor<B>,
|
||||
state: PeerState,
|
||||
}
|
||||
|
||||
/// Syncing strategy that downloads and imports a recent state directly.
|
||||
pub struct StateStrategy<B: BlockT> {
|
||||
state_sync: Box<dyn StateSyncProvider<B>>,
|
||||
peers: HashMap<PeerId, Peer<B>>,
|
||||
actions: Vec<StateStrategyAction<B>>,
|
||||
succeded: bool,
|
||||
}
|
||||
|
||||
impl<B: BlockT> StateStrategy<B> {
|
||||
/// Create a new instance.
|
||||
pub fn new<Client>(
|
||||
client: Arc<Client>,
|
||||
target_header: B::Header,
|
||||
target_body: Option<Vec<B::Extrinsic>>,
|
||||
target_justifications: Option<Justifications>,
|
||||
skip_proof: bool,
|
||||
initial_peers: impl Iterator<Item = (PeerId, NumberFor<B>)>,
|
||||
) -> Self
|
||||
where
|
||||
Client: ProofProvider<B> + Send + Sync + 'static,
|
||||
{
|
||||
let peers = initial_peers
|
||||
.map(|(peer_id, best_number)| {
|
||||
(peer_id, Peer { best_number, state: PeerState::Available })
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
state_sync: Box::new(StateSync::new(
|
||||
client,
|
||||
target_header,
|
||||
target_body,
|
||||
target_justifications,
|
||||
skip_proof,
|
||||
)),
|
||||
peers,
|
||||
actions: Vec::new(),
|
||||
succeded: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new instance with a custom state sync provider.
|
||||
// Used in tests.
|
||||
#[cfg(test)]
|
||||
fn new_with_provider(
|
||||
state_sync_provider: Box<dyn StateSyncProvider<B>>,
|
||||
initial_peers: impl Iterator<Item = (PeerId, NumberFor<B>)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
state_sync: state_sync_provider,
|
||||
peers: initial_peers
|
||||
.map(|(peer_id, best_number)| {
|
||||
(peer_id, Peer { best_number, state: PeerState::Available })
|
||||
})
|
||||
.collect(),
|
||||
actions: Vec::new(),
|
||||
succeded: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify that a new peer has connected.
|
||||
pub fn add_peer(&mut self, peer_id: PeerId, _best_hash: B::Hash, best_number: NumberFor<B>) {
|
||||
self.peers.insert(peer_id, Peer { best_number, state: PeerState::Available });
|
||||
}
|
||||
|
||||
/// Notify that a peer has disconnected.
|
||||
pub fn remove_peer(&mut self, peer_id: &PeerId) {
|
||||
self.peers.remove(peer_id);
|
||||
}
|
||||
|
||||
/// Submit a validated block announcement.
|
||||
///
|
||||
/// Returns new best hash & best number of the peer if they are updated.
|
||||
#[must_use]
|
||||
pub fn on_validated_block_announce(
|
||||
&mut self,
|
||||
is_best: bool,
|
||||
peer_id: PeerId,
|
||||
announce: &BlockAnnounce<B::Header>,
|
||||
) -> Option<(B::Hash, NumberFor<B>)> {
|
||||
is_best.then_some({
|
||||
let best_number = *announce.header.number();
|
||||
let best_hash = announce.header.hash();
|
||||
if let Some(ref mut peer) = self.peers.get_mut(&peer_id) {
|
||||
peer.best_number = best_number;
|
||||
}
|
||||
// Let `SyncingEngine` know that we should update the peer info.
|
||||
(best_hash, best_number)
|
||||
})
|
||||
}
|
||||
|
||||
/// Process state response.
|
||||
pub fn on_state_response(&mut self, peer_id: PeerId, response: OpaqueStateResponse) {
|
||||
if let Err(bad_peer) = self.on_state_response_inner(peer_id, response) {
|
||||
self.actions.push(StateStrategyAction::DropPeer(bad_peer));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_state_response_inner(
|
||||
&mut self,
|
||||
peer_id: PeerId,
|
||||
response: OpaqueStateResponse,
|
||||
) -> Result<(), BadPeer> {
|
||||
if let Some(peer) = self.peers.get_mut(&peer_id) {
|
||||
peer.state = PeerState::Available;
|
||||
}
|
||||
|
||||
let response: Box<StateResponse> = response.0.downcast().map_err(|_error| {
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to downcast opaque state response, this is an implementation bug."
|
||||
);
|
||||
debug_assert!(false);
|
||||
|
||||
BadPeer(peer_id, rep::BAD_RESPONSE)
|
||||
})?;
|
||||
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Importing state data from {} with {} keys, {} proof nodes.",
|
||||
peer_id,
|
||||
response.entries.len(),
|
||||
response.proof.len(),
|
||||
);
|
||||
|
||||
match self.state_sync.import(*response) {
|
||||
ImportResult::Import(hash, header, state, body, justifications) => {
|
||||
let origin = BlockOrigin::NetworkInitialSync;
|
||||
let block = IncomingBlock {
|
||||
hash,
|
||||
header: Some(header),
|
||||
body,
|
||||
indexed_body: None,
|
||||
justifications,
|
||||
origin: None,
|
||||
allow_missing_state: true,
|
||||
import_existing: true,
|
||||
skip_execution: true,
|
||||
state: Some(state),
|
||||
};
|
||||
debug!(target: LOG_TARGET, "State download is complete. Import is queued");
|
||||
self.actions
|
||||
.push(StateStrategyAction::ImportBlocks { origin, blocks: vec![block] });
|
||||
Ok(())
|
||||
},
|
||||
ImportResult::Continue => Ok(()),
|
||||
ImportResult::BadResponse => {
|
||||
debug!(target: LOG_TARGET, "Bad state data received from {peer_id}");
|
||||
Err(BadPeer(peer_id, rep::BAD_STATE))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A batch of blocks have been processed, with or without errors.
|
||||
///
|
||||
/// Normally this should be called when target block with state is imported.
|
||||
pub fn on_blocks_processed(
|
||||
&mut self,
|
||||
imported: usize,
|
||||
count: usize,
|
||||
results: Vec<(Result<BlockImportStatus<NumberFor<B>>, BlockImportError>, B::Hash)>,
|
||||
) {
|
||||
trace!(target: LOG_TARGET, "State sync: imported {imported} of {count}.");
|
||||
|
||||
let results = results
|
||||
.into_iter()
|
||||
.filter_map(|(result, hash)| {
|
||||
if hash == self.state_sync.target_hash() {
|
||||
Some(result)
|
||||
} else {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Unexpected block processed: {hash} with result {result:?}.",
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !results.is_empty() {
|
||||
// We processed the target block
|
||||
results.iter().filter_map(|result| result.as_ref().err()).for_each(|e| {
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to import target block with state: {e:?}."
|
||||
);
|
||||
});
|
||||
self.succeded |= results.into_iter().any(|result| result.is_ok());
|
||||
self.actions.push(StateStrategyAction::Finished);
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce state request.
|
||||
fn state_request(&mut self) -> Option<(PeerId, OpaqueStateRequest)> {
|
||||
if self.state_sync.is_complete() {
|
||||
return None
|
||||
}
|
||||
|
||||
if self
|
||||
.peers
|
||||
.values()
|
||||
.any(|peer| matches!(peer.state, PeerState::DownloadingState))
|
||||
{
|
||||
// Only one state request at a time is possible.
|
||||
return None
|
||||
}
|
||||
|
||||
let peer_id =
|
||||
self.schedule_next_peer(PeerState::DownloadingState, self.state_sync.target_number())?;
|
||||
let request = self.state_sync.next_request();
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"New state request to {peer_id}: {request:?}.",
|
||||
);
|
||||
Some((peer_id, OpaqueStateRequest(Box::new(request))))
|
||||
}
|
||||
|
||||
fn schedule_next_peer(
|
||||
&mut self,
|
||||
new_state: PeerState,
|
||||
min_best_number: NumberFor<B>,
|
||||
) -> Option<PeerId> {
|
||||
let mut targets: Vec<_> = self.peers.values().map(|p| p.best_number).collect();
|
||||
if targets.is_empty() {
|
||||
return None
|
||||
}
|
||||
targets.sort();
|
||||
let median = targets[targets.len() / 2];
|
||||
let threshold = std::cmp::max(median, min_best_number);
|
||||
// Find a random peer that is synced as much as peer majority and is above
|
||||
// `min_best_number`.
|
||||
for (peer_id, peer) in self.peers.iter_mut() {
|
||||
if peer.state.is_available() && peer.best_number >= threshold {
|
||||
peer.state = new_state;
|
||||
return Some(*peer_id)
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the current sync status.
|
||||
pub fn status(&self) -> SyncStatus<B> {
|
||||
SyncStatus {
|
||||
state: if self.state_sync.is_complete() {
|
||||
SyncState::Idle
|
||||
} else {
|
||||
SyncState::Downloading { target: self.state_sync.target_number() }
|
||||
},
|
||||
best_seen_block: Some(self.state_sync.target_number()),
|
||||
num_peers: self.peers.len().saturated_into(),
|
||||
num_connected_peers: self.peers.len().saturated_into(),
|
||||
queued_blocks: 0,
|
||||
state_sync: Some(self.state_sync.progress()),
|
||||
warp_sync: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of peers known to syncing.
|
||||
pub fn num_peers(&self) -> usize {
|
||||
self.peers.len()
|
||||
}
|
||||
|
||||
/// Get actions that should be performed by the owner on [`WarpSync`]'s behalf
|
||||
#[must_use]
|
||||
pub fn actions(&mut self) -> impl Iterator<Item = StateStrategyAction<B>> {
|
||||
let state_request = self
|
||||
.state_request()
|
||||
.into_iter()
|
||||
.map(|(peer_id, request)| StateStrategyAction::SendStateRequest { peer_id, request });
|
||||
self.actions.extend(state_request);
|
||||
|
||||
std::mem::take(&mut self.actions).into_iter()
|
||||
}
|
||||
|
||||
/// Check if state sync has succeded.
|
||||
#[must_use]
|
||||
pub fn is_succeded(&self) -> bool {
|
||||
self.succeded
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
schema::v1::{StateRequest, StateResponse},
|
||||
strategy::state_sync::{ImportResult, StateSyncProgress, StateSyncProvider},
|
||||
};
|
||||
use codec::Decode;
|
||||
use sc_block_builder::BlockBuilderBuilder;
|
||||
use sc_client_api::KeyValueStates;
|
||||
use sc_consensus::{ImportedAux, ImportedState};
|
||||
use sp_runtime::traits::Zero;
|
||||
use substrate_test_runtime_client::{
|
||||
runtime::{Block, Hash},
|
||||
BlockBuilderExt, DefaultTestClientBuilderExt, TestClientBuilder, TestClientBuilderExt,
|
||||
};
|
||||
|
||||
mockall::mock! {
|
||||
pub StateSync<B: BlockT> {}
|
||||
|
||||
impl<B: BlockT> StateSyncProvider<B> for StateSync<B> {
|
||||
fn import(&mut self, response: StateResponse) -> ImportResult<B>;
|
||||
fn next_request(&self) -> StateRequest;
|
||||
fn is_complete(&self) -> bool;
|
||||
fn target_number(&self) -> NumberFor<B>;
|
||||
fn target_hash(&self) -> B::Hash;
|
||||
fn progress(&self) -> StateSyncProgress;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_peer_is_scheduled_if_no_peers_connected() {
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let target_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
let target_header = target_block.header().clone();
|
||||
|
||||
let mut state_strategy =
|
||||
StateStrategy::new(client, target_header, None, None, false, std::iter::empty());
|
||||
|
||||
assert!(state_strategy
|
||||
.schedule_next_peer(PeerState::DownloadingState, Zero::zero())
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_least_median_synced_peer_is_scheduled() {
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let target_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
|
||||
for _ in 0..100 {
|
||||
let peers = (1..=10)
|
||||
.map(|best_number| (PeerId::random(), best_number))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let initial_peers = peers.iter().map(|(p, n)| (*p, *n));
|
||||
|
||||
let mut state_strategy = StateStrategy::new(
|
||||
client.clone(),
|
||||
target_block.header().clone(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
initial_peers,
|
||||
);
|
||||
|
||||
let peer_id =
|
||||
state_strategy.schedule_next_peer(PeerState::DownloadingState, Zero::zero());
|
||||
assert!(*peers.get(&peer_id.unwrap()).unwrap() >= 6);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_best_number_peer_is_scheduled() {
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let target_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
|
||||
for _ in 0..10 {
|
||||
let peers = (1..=10)
|
||||
.map(|best_number| (PeerId::random(), best_number))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let initial_peers = peers.iter().map(|(p, n)| (*p, *n));
|
||||
|
||||
let mut state_strategy = StateStrategy::new(
|
||||
client.clone(),
|
||||
target_block.header().clone(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
initial_peers,
|
||||
);
|
||||
|
||||
let peer_id = state_strategy.schedule_next_peer(PeerState::DownloadingState, 10);
|
||||
assert!(*peers.get(&peer_id.unwrap()).unwrap() == 10);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_request_contains_correct_hash() {
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let target_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
|
||||
let initial_peers = (1..=10).map(|best_number| (PeerId::random(), best_number));
|
||||
|
||||
let mut state_strategy = StateStrategy::new(
|
||||
client.clone(),
|
||||
target_block.header().clone(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
initial_peers,
|
||||
);
|
||||
|
||||
let (_peer_id, mut opaque_request) = state_strategy.state_request().unwrap();
|
||||
let request: &mut StateRequest = opaque_request.0.downcast_mut().unwrap();
|
||||
let hash = Hash::decode(&mut &*request.block).unwrap();
|
||||
|
||||
assert_eq!(hash, target_block.header().hash());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_parallel_state_requests() {
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let target_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
|
||||
let initial_peers = (1..=10).map(|best_number| (PeerId::random(), best_number));
|
||||
|
||||
let mut state_strategy = StateStrategy::new(
|
||||
client.clone(),
|
||||
target_block.header().clone(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
initial_peers,
|
||||
);
|
||||
|
||||
// First request is sent.
|
||||
assert!(state_strategy.state_request().is_some());
|
||||
|
||||
// No parallel request is sent.
|
||||
assert!(state_strategy.state_request().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn received_state_response_makes_peer_available_again() {
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
state_sync_provider.expect_import().return_once(|_| ImportResult::Continue);
|
||||
let peer_id = PeerId::random();
|
||||
let initial_peers = std::iter::once((peer_id, 10));
|
||||
let mut state_strategy =
|
||||
StateStrategy::new_with_provider(Box::new(state_sync_provider), initial_peers);
|
||||
// Manually set the peer's state.
|
||||
state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
|
||||
|
||||
let dummy_response = OpaqueStateResponse(Box::new(StateResponse::default()));
|
||||
state_strategy.on_state_response(peer_id, dummy_response);
|
||||
|
||||
assert!(state_strategy.peers.get(&peer_id).unwrap().state.is_available());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_state_response_drops_peer() {
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
// Provider says that state response is bad.
|
||||
state_sync_provider.expect_import().return_once(|_| ImportResult::BadResponse);
|
||||
let peer_id = PeerId::random();
|
||||
let initial_peers = std::iter::once((peer_id, 10));
|
||||
let mut state_strategy =
|
||||
StateStrategy::new_with_provider(Box::new(state_sync_provider), initial_peers);
|
||||
// Manually set the peer's state.
|
||||
state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
|
||||
let dummy_response = OpaqueStateResponse(Box::new(StateResponse::default()));
|
||||
// Receiving response drops the peer.
|
||||
assert!(matches!(
|
||||
state_strategy.on_state_response_inner(peer_id, dummy_response),
|
||||
Err(BadPeer(id, _rep)) if id == peer_id,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_state_response_doesnt_generate_actions() {
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
// Sync provider says that the response is partial.
|
||||
state_sync_provider.expect_import().return_once(|_| ImportResult::Continue);
|
||||
let peer_id = PeerId::random();
|
||||
let initial_peers = std::iter::once((peer_id, 10));
|
||||
let mut state_strategy =
|
||||
StateStrategy::new_with_provider(Box::new(state_sync_provider), initial_peers);
|
||||
// Manually set the peer's state .
|
||||
state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
|
||||
|
||||
let dummy_response = OpaqueStateResponse(Box::new(StateResponse::default()));
|
||||
state_strategy.on_state_response(peer_id, dummy_response);
|
||||
|
||||
// No actions generated.
|
||||
assert_eq!(state_strategy.actions.len(), 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_state_response_leads_to_block_import() {
|
||||
// Build block to use for checks.
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let mut block_builder = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap();
|
||||
block_builder.push_storage_change(vec![1, 2, 3], Some(vec![4, 5, 6])).unwrap();
|
||||
let block = block_builder.build().unwrap().block;
|
||||
let header = block.header().clone();
|
||||
let hash = header.hash();
|
||||
let body = Some(block.extrinsics().iter().cloned().collect::<Vec<_>>());
|
||||
let state = ImportedState { block: hash, state: KeyValueStates(Vec::new()) };
|
||||
let justifications = Some(Justifications::from((*b"FRNK", Vec::new())));
|
||||
|
||||
// Prepare `StateSync`
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
let import = ImportResult::Import(
|
||||
hash,
|
||||
header.clone(),
|
||||
state.clone(),
|
||||
body.clone(),
|
||||
justifications.clone(),
|
||||
);
|
||||
state_sync_provider.expect_import().return_once(move |_| import);
|
||||
|
||||
// Reference values to check against.
|
||||
let expected_origin = BlockOrigin::NetworkInitialSync;
|
||||
let expected_block = IncomingBlock {
|
||||
hash,
|
||||
header: Some(header),
|
||||
body,
|
||||
indexed_body: None,
|
||||
justifications,
|
||||
origin: None,
|
||||
allow_missing_state: true,
|
||||
import_existing: true,
|
||||
skip_execution: true,
|
||||
state: Some(state),
|
||||
};
|
||||
let expected_blocks = vec![expected_block];
|
||||
|
||||
// Prepare `StateStrategy`.
|
||||
let peer_id = PeerId::random();
|
||||
let initial_peers = std::iter::once((peer_id, 10));
|
||||
let mut state_strategy =
|
||||
StateStrategy::new_with_provider(Box::new(state_sync_provider), initial_peers);
|
||||
// Manually set the peer's state .
|
||||
state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
|
||||
|
||||
// Receive response.
|
||||
let dummy_response = OpaqueStateResponse(Box::new(StateResponse::default()));
|
||||
state_strategy.on_state_response(peer_id, dummy_response);
|
||||
|
||||
assert_eq!(state_strategy.actions.len(), 1);
|
||||
assert!(matches!(
|
||||
&state_strategy.actions[0],
|
||||
StateStrategyAction::ImportBlocks { origin, blocks }
|
||||
if *origin == expected_origin && *blocks == expected_blocks,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn importing_unknown_block_doesnt_finish_strategy() {
|
||||
let target_hash = Hash::random();
|
||||
let unknown_hash = Hash::random();
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
state_sync_provider.expect_target_hash().return_const(target_hash);
|
||||
|
||||
let mut state_strategy =
|
||||
StateStrategy::new_with_provider(Box::new(state_sync_provider), std::iter::empty());
|
||||
|
||||
// Unknown block imported.
|
||||
state_strategy.on_blocks_processed(
|
||||
1,
|
||||
1,
|
||||
vec![(
|
||||
Ok(BlockImportStatus::ImportedUnknown(1, ImportedAux::default(), None)),
|
||||
unknown_hash,
|
||||
)],
|
||||
);
|
||||
|
||||
// No actions generated.
|
||||
assert_eq!(state_strategy.actions.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn succesfully_importing_target_block_finishes_strategy() {
|
||||
let target_hash = Hash::random();
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
state_sync_provider.expect_target_hash().return_const(target_hash);
|
||||
|
||||
let mut state_strategy =
|
||||
StateStrategy::new_with_provider(Box::new(state_sync_provider), std::iter::empty());
|
||||
|
||||
// Target block imported.
|
||||
state_strategy.on_blocks_processed(
|
||||
1,
|
||||
1,
|
||||
vec![(
|
||||
Ok(BlockImportStatus::ImportedUnknown(1, ImportedAux::default(), None)),
|
||||
target_hash,
|
||||
)],
|
||||
);
|
||||
|
||||
// Strategy finishes.
|
||||
assert_eq!(state_strategy.actions.len(), 1);
|
||||
assert!(matches!(&state_strategy.actions[0], StateStrategyAction::Finished));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failure_to_import_target_block_finishes_strategy() {
|
||||
let target_hash = Hash::random();
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
state_sync_provider.expect_target_hash().return_const(target_hash);
|
||||
|
||||
let mut state_strategy =
|
||||
StateStrategy::new_with_provider(Box::new(state_sync_provider), std::iter::empty());
|
||||
|
||||
// Target block import failed.
|
||||
state_strategy.on_blocks_processed(
|
||||
1,
|
||||
1,
|
||||
vec![(
|
||||
Err(BlockImportError::VerificationFailed(None, String::from("test-error"))),
|
||||
target_hash,
|
||||
)],
|
||||
);
|
||||
|
||||
// Strategy finishes.
|
||||
assert_eq!(state_strategy.actions.len(), 1);
|
||||
assert!(matches!(&state_strategy.actions[0], StateStrategyAction::Finished));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finished_strategy_doesnt_generate_more_actions() {
|
||||
let target_hash = Hash::random();
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
state_sync_provider.expect_target_hash().return_const(target_hash);
|
||||
state_sync_provider.expect_is_complete().return_const(true);
|
||||
|
||||
// Get enough peers for possible spurious requests.
|
||||
let initial_peers = (1..=10).map(|best_number| (PeerId::random(), best_number));
|
||||
|
||||
let mut state_strategy =
|
||||
StateStrategy::new_with_provider(Box::new(state_sync_provider), initial_peers);
|
||||
|
||||
state_strategy.on_blocks_processed(
|
||||
1,
|
||||
1,
|
||||
vec![(
|
||||
Ok(BlockImportStatus::ImportedUnknown(1, ImportedAux::default(), None)),
|
||||
target_hash,
|
||||
)],
|
||||
);
|
||||
|
||||
// Strategy finishes.
|
||||
let actions = state_strategy.actions().collect::<Vec<_>>();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert!(matches!(&actions[0], StateStrategyAction::Finished));
|
||||
|
||||
// No more actions generated.
|
||||
assert_eq!(state_strategy.actions().count(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! State sync support.
|
||||
|
||||
use crate::{
|
||||
schema::v1::{StateEntry, StateRequest, StateResponse},
|
||||
LOG_TARGET,
|
||||
};
|
||||
use codec::{Decode, Encode};
|
||||
use log::debug;
|
||||
use sc_client_api::{CompactProof, ProofProvider};
|
||||
use sc_consensus::ImportedState;
|
||||
use smallvec::SmallVec;
|
||||
use sp_core::storage::well_known_keys;
|
||||
use sp_runtime::{
|
||||
traits::{Block as BlockT, Header, NumberFor},
|
||||
Justifications,
|
||||
};
|
||||
use std::{collections::HashMap, fmt, sync::Arc};
|
||||
|
||||
/// Generic state sync provider. Used for mocking in tests.
|
||||
pub trait StateSyncProvider<B: BlockT>: Send + Sync {
|
||||
/// Validate and import a state response.
|
||||
fn import(&mut self, response: StateResponse) -> ImportResult<B>;
|
||||
/// Produce next state request.
|
||||
fn next_request(&self) -> StateRequest;
|
||||
/// Check if the state is complete.
|
||||
fn is_complete(&self) -> bool;
|
||||
/// Returns target block number.
|
||||
fn target_number(&self) -> NumberFor<B>;
|
||||
/// Returns target block hash.
|
||||
fn target_hash(&self) -> B::Hash;
|
||||
/// Returns state sync estimated progress.
|
||||
fn progress(&self) -> StateSyncProgress;
|
||||
}
|
||||
|
||||
// Reported state sync phase.
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
pub enum StateSyncPhase {
|
||||
// State download in progress.
|
||||
DownloadingState,
|
||||
// Download is complete, state is being imported.
|
||||
ImportingState,
|
||||
}
|
||||
|
||||
impl fmt::Display for StateSyncPhase {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::DownloadingState => write!(f, "Downloading state"),
|
||||
Self::ImportingState => write!(f, "Importing state"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reported state download progress.
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
pub struct StateSyncProgress {
|
||||
/// Estimated download percentage.
|
||||
pub percentage: u32,
|
||||
/// Total state size in bytes downloaded so far.
|
||||
pub size: u64,
|
||||
/// Current state sync phase.
|
||||
pub phase: StateSyncPhase,
|
||||
}
|
||||
|
||||
/// Import state chunk result.
|
||||
pub enum ImportResult<B: BlockT> {
|
||||
/// State is complete and ready for import.
|
||||
Import(B::Hash, B::Header, ImportedState<B>, Option<Vec<B::Extrinsic>>, Option<Justifications>),
|
||||
/// Continue downloading.
|
||||
Continue,
|
||||
/// Bad state chunk.
|
||||
BadResponse,
|
||||
}
|
||||
|
||||
/// State sync state machine. Accumulates partial state data until it
|
||||
/// is ready to be imported.
|
||||
pub struct StateSync<B: BlockT, Client> {
|
||||
target_block: B::Hash,
|
||||
target_header: B::Header,
|
||||
target_root: B::Hash,
|
||||
target_body: Option<Vec<B::Extrinsic>>,
|
||||
target_justifications: Option<Justifications>,
|
||||
last_key: SmallVec<[Vec<u8>; 2]>,
|
||||
state: HashMap<Vec<u8>, (Vec<(Vec<u8>, Vec<u8>)>, Vec<Vec<u8>>)>,
|
||||
complete: bool,
|
||||
client: Arc<Client>,
|
||||
imported_bytes: u64,
|
||||
skip_proof: bool,
|
||||
}
|
||||
|
||||
impl<B, Client> StateSync<B, Client>
|
||||
where
|
||||
B: BlockT,
|
||||
Client: ProofProvider<B> + Send + Sync + 'static,
|
||||
{
|
||||
/// Create a new instance.
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
target_header: B::Header,
|
||||
target_body: Option<Vec<B::Extrinsic>>,
|
||||
target_justifications: Option<Justifications>,
|
||||
skip_proof: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
client,
|
||||
target_block: target_header.hash(),
|
||||
target_root: *target_header.state_root(),
|
||||
target_header,
|
||||
target_body,
|
||||
target_justifications,
|
||||
last_key: SmallVec::default(),
|
||||
state: HashMap::default(),
|
||||
complete: false,
|
||||
imported_bytes: 0,
|
||||
skip_proof,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, Client> StateSyncProvider<B> for StateSync<B, Client>
|
||||
where
|
||||
B: BlockT,
|
||||
Client: ProofProvider<B> + Send + Sync + 'static,
|
||||
{
|
||||
/// Validate and import a state response.
|
||||
fn import(&mut self, response: StateResponse) -> ImportResult<B> {
|
||||
if response.entries.is_empty() && response.proof.is_empty() {
|
||||
debug!(target: LOG_TARGET, "Bad state response");
|
||||
return ImportResult::BadResponse
|
||||
}
|
||||
if !self.skip_proof && response.proof.is_empty() {
|
||||
debug!(target: LOG_TARGET, "Missing proof");
|
||||
return ImportResult::BadResponse
|
||||
}
|
||||
let complete = if !self.skip_proof {
|
||||
debug!(target: LOG_TARGET, "Importing state from {} trie nodes", response.proof.len());
|
||||
let proof_size = response.proof.len() as u64;
|
||||
let proof = match CompactProof::decode(&mut response.proof.as_ref()) {
|
||||
Ok(proof) => proof,
|
||||
Err(e) => {
|
||||
debug!(target: LOG_TARGET, "Error decoding proof: {:?}", e);
|
||||
return ImportResult::BadResponse
|
||||
},
|
||||
};
|
||||
let (values, completed) = match self.client.verify_range_proof(
|
||||
self.target_root,
|
||||
proof,
|
||||
self.last_key.as_slice(),
|
||||
) {
|
||||
Err(e) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"StateResponse failed proof verification: {}",
|
||||
e,
|
||||
);
|
||||
return ImportResult::BadResponse
|
||||
},
|
||||
Ok(values) => values,
|
||||
};
|
||||
debug!(target: LOG_TARGET, "Imported with {} keys", values.len());
|
||||
|
||||
let complete = completed == 0;
|
||||
if !complete && !values.update_last_key(completed, &mut self.last_key) {
|
||||
debug!(target: LOG_TARGET, "Error updating key cursor, depth: {}", completed);
|
||||
};
|
||||
|
||||
for values in values.0 {
|
||||
let key_values = if values.state_root.is_empty() {
|
||||
// Read child trie roots.
|
||||
values
|
||||
.key_values
|
||||
.into_iter()
|
||||
.filter(|key_value| {
|
||||
if well_known_keys::is_child_storage_key(key_value.0.as_slice()) {
|
||||
self.state
|
||||
.entry(key_value.1.clone())
|
||||
.or_default()
|
||||
.1
|
||||
.push(key_value.0.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
values.key_values
|
||||
};
|
||||
let entry = self.state.entry(values.state_root).or_default();
|
||||
if entry.0.len() > 0 && entry.1.len() > 1 {
|
||||
// Already imported child_trie with same root.
|
||||
// Warning this will not work with parallel download.
|
||||
} else if entry.0.is_empty() {
|
||||
for (key, _value) in key_values.iter() {
|
||||
self.imported_bytes += key.len() as u64;
|
||||
}
|
||||
|
||||
entry.0 = key_values;
|
||||
} else {
|
||||
for (key, value) in key_values {
|
||||
self.imported_bytes += key.len() as u64;
|
||||
entry.0.push((key, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
self.imported_bytes += proof_size;
|
||||
complete
|
||||
} else {
|
||||
let mut complete = true;
|
||||
// if the trie is a child trie and one of its parent trie is empty,
|
||||
// the parent cursor stays valid.
|
||||
// Empty parent trie content only happens when all the response content
|
||||
// is part of a single child trie.
|
||||
if self.last_key.len() == 2 && response.entries[0].entries.is_empty() {
|
||||
// Do not remove the parent trie position.
|
||||
self.last_key.pop();
|
||||
} else {
|
||||
self.last_key.clear();
|
||||
}
|
||||
for state in response.entries {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Importing state from {:?} to {:?}",
|
||||
state.entries.last().map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key)),
|
||||
state.entries.first().map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key)),
|
||||
);
|
||||
|
||||
if !state.complete {
|
||||
if let Some(e) = state.entries.last() {
|
||||
self.last_key.push(e.key.clone());
|
||||
}
|
||||
complete = false;
|
||||
}
|
||||
let is_top = state.state_root.is_empty();
|
||||
let entry = self.state.entry(state.state_root).or_default();
|
||||
if entry.0.len() > 0 && entry.1.len() > 1 {
|
||||
// Already imported child trie with same root.
|
||||
} else {
|
||||
let mut child_roots = Vec::new();
|
||||
for StateEntry { key, value } in state.entries {
|
||||
// Skip all child key root (will be recalculated on import).
|
||||
if is_top && well_known_keys::is_child_storage_key(key.as_slice()) {
|
||||
child_roots.push((value, key));
|
||||
} else {
|
||||
self.imported_bytes += key.len() as u64;
|
||||
entry.0.push((key, value))
|
||||
}
|
||||
}
|
||||
for (root, storage_key) in child_roots {
|
||||
self.state.entry(root).or_default().1.push(storage_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
complete
|
||||
};
|
||||
if complete {
|
||||
self.complete = true;
|
||||
ImportResult::Import(
|
||||
self.target_block,
|
||||
self.target_header.clone(),
|
||||
ImportedState {
|
||||
block: self.target_block,
|
||||
state: std::mem::take(&mut self.state).into(),
|
||||
},
|
||||
self.target_body.clone(),
|
||||
self.target_justifications.clone(),
|
||||
)
|
||||
} else {
|
||||
ImportResult::Continue
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce next state request.
|
||||
fn next_request(&self) -> StateRequest {
|
||||
StateRequest {
|
||||
block: self.target_block.encode(),
|
||||
start: self.last_key.clone().into_vec(),
|
||||
no_proof: self.skip_proof,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the state is complete.
|
||||
fn is_complete(&self) -> bool {
|
||||
self.complete
|
||||
}
|
||||
|
||||
/// Returns target block number.
|
||||
fn target_number(&self) -> NumberFor<B> {
|
||||
*self.target_header.number()
|
||||
}
|
||||
|
||||
/// Returns target block hash.
|
||||
fn target_hash(&self) -> B::Hash {
|
||||
self.target_block
|
||||
}
|
||||
|
||||
/// Returns state sync estimated progress.
|
||||
fn progress(&self) -> StateSyncProgress {
|
||||
let cursor = *self.last_key.get(0).and_then(|last| last.get(0)).unwrap_or(&0u8);
|
||||
let percent_done = cursor as u32 * 100 / 256;
|
||||
StateSyncProgress {
|
||||
percentage: percent_done,
|
||||
size: self.imported_bytes,
|
||||
phase: if self.complete {
|
||||
StateSyncPhase::ImportingState
|
||||
} else {
|
||||
StateSyncPhase::DownloadingState
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user