Skip to content

Commit d2ec812

Browse files
authored
fix: bitcoin adapter: prune blocks if response empty (#3775)
Before, blocks were only pruned if they were: either processed by the canister, or below (less height) the anchor. After, in addition to the above, all blocks are also pruned if the response contained no blocks. The reason why this is safe is because during the BFS traversal, we try to return at least one cached block that is reachable from the anchor. If no block was found, this indicates that no block in the cache can be reached from the anchor, so they can be safely discarded. Secondly, this is also needed because the cache can get full of blocks which are above the height of the anchor, but can't be served currently (there is for example a block that's missing between them and the anchor). And if the cache is full of blocks, the adapter won't be able to request any more new blocks.
1 parent 00be225 commit d2ec812

File tree

4 files changed

+64
-50
lines changed

4 files changed

+64
-50
lines changed

rs/bitcoin/adapter/src/blockchainmanager.rs

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,6 @@ const MAX_UNSOLICITED_HEADERS: usize = 20;
4242
/// to a peer at a time.
4343
const INV_PER_GET_DATA_REQUEST: u32 = 8;
4444

45-
const ONE_MB: usize = 1_024 * 1_024;
46-
47-
/// The limit at which we should stop making additional requests for new blocks as the block cache
48-
/// becomes too large. Inflight `getdata` messages will remain active, but new `getdata` messages will
49-
/// not be created.
50-
const BLOCK_CACHE_THRESHOLD_BYTES: usize = 10 * ONE_MB;
51-
5245
/// Block locators. Consists of starting hashes and a stop hash.
5346
type Locators = (Vec<BlockHash>, BlockHash);
5447

@@ -540,17 +533,12 @@ impl BlockchainManager {
540533
return;
541534
}
542535

543-
let block_cache_size = self.blockchain.lock().unwrap().get_block_cache_size();
536+
let is_cache_full = self.blockchain.lock().unwrap().is_block_cache_full();
544537

545-
if block_cache_size >= BLOCK_CACHE_THRESHOLD_BYTES {
546-
debug!(
547-
self.logger,
548-
"Cache Size: {}, Max Size: {}", block_cache_size, BLOCK_CACHE_THRESHOLD_BYTES
549-
);
538+
if is_cache_full {
539+
debug!(self.logger, "Cache full");
550540
}
551541

552-
let is_cache_full = block_cache_size >= BLOCK_CACHE_THRESHOLD_BYTES;
553-
554542
// Count the number of requests per peer.
555543
let mut requests_per_peer: HashMap<SocketAddr, u32> =
556544
self.peer_info.keys().map(|addr| (*addr, 0)).collect();
@@ -1302,7 +1290,7 @@ pub mod test {
13021290
blockchain.add_block(block).expect("failed to add block");
13031291
}
13041292

1305-
assert!(blockchain.get_block_cache_size() >= BLOCK_CACHE_THRESHOLD_BYTES);
1293+
assert!(blockchain.is_block_cache_full());
13061294
}
13071295

13081296
let block_1_hash = large_blockchain_headers

rs/bitcoin/adapter/src/blockchainstate.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ use thiserror::Error;
1212

1313
use bitcoin::Work;
1414

15+
/// The limit at which we should stop making additional requests for new blocks as the block cache
16+
/// becomes too large. Inflight `getdata` messages will remain active, but new `getdata` messages will
17+
/// not be created.
18+
const BLOCK_CACHE_THRESHOLD_BYTES: usize = 10 * ONE_MB;
19+
const ONE_MB: usize = 1_024 * 1_024;
20+
1521
/// Contains the necessary information about a tip.
1622
#[derive(Clone, Debug)]
1723
pub struct Tip {
@@ -137,6 +143,11 @@ impl BlockchainState {
137143
self.header_cache.get(hash)
138144
}
139145

146+
/// Returns the hashes of all cached blocks.
147+
pub(crate) fn get_cached_blocks(&self) -> Vec<BlockHash> {
148+
self.block_cache.keys().copied().collect()
149+
}
150+
140151
/// Processes the `headers` message received from Bitcoin nodes by adding them to the state.
141152
/// Headers are expected to be sorted. If they are not, the headers will be likely be rejected
142153
/// with a [AddHeaderError::PrevHeaderNotCached](AddHeaderError::PrevHeaderNotCached) error.
@@ -328,6 +339,10 @@ impl BlockchainState {
328339
self.block_cache = HashMap::new();
329340
}
330341

342+
pub(crate) fn is_block_cache_full(&self) -> bool {
343+
self.get_block_cache_size() >= BLOCK_CACHE_THRESHOLD_BYTES
344+
}
345+
331346
/// Returns the current size of the block cache.
332347
pub fn get_block_cache_size(&self) -> usize {
333348
self.block_cache

rs/bitcoin/adapter/src/get_successors_handler.rs

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ impl GetSuccessorsHandler {
118118
.processed_block_hashes
119119
.observe(request.processed_block_hashes.len() as f64);
120120

121-
let (blocks, next) = {
121+
let (blocks, next, obsolete_blocks) = {
122122
let state = self.state.lock().unwrap();
123123
let anchor_height = state
124124
.get_cached_header(&request.anchor)
@@ -139,7 +139,17 @@ impl GetSuccessorsHandler {
139139
&blocks,
140140
self.network,
141141
);
142-
(blocks, next)
142+
// If no blocks are returned, this means that nothing that is in the cache could be reached from the anchor.
143+
// We can safely remove everything that is in the cache then, as those blocks are no longer needed.
144+
// There is a chance that these blocks are above the anchor height (but they were forked below it),
145+
// meaning that the regular "pruning anything below anchor" will not affect them.
146+
// There is also a chance that they are reachable from the anchor, just not through the cache.
147+
// Meaning that we still need to download some other blocks first. (hence we need to free the cache).
148+
let mut obsolete_blocks = request.processed_block_hashes;
149+
if blocks.is_empty() && state.is_block_cache_full() {
150+
obsolete_blocks.extend(state.get_cached_blocks())
151+
}
152+
(blocks, next, obsolete_blocks)
143153
};
144154
let response_next = &next[..next.len().min(MAX_NEXT_BLOCK_HEADERS_LENGTH)];
145155
let response = GetSuccessorsResponse {
@@ -160,7 +170,7 @@ impl GetSuccessorsHandler {
160170
self.blockchain_manager_tx
161171
.try_send(BlockchainManagerRequest::PruneBlocks(
162172
request.anchor,
163-
request.processed_block_hashes,
173+
obsolete_blocks,
164174
))
165175
.ok();
166176

@@ -195,30 +205,31 @@ fn get_successor_blocks(
195205
_ => MAX_BLOCKS_BYTES,
196206
};
197207

208+
let max_blocks_length = if allow_multiple_blocks {
209+
MAX_BLOCKS_LENGTH
210+
} else {
211+
1
212+
};
213+
198214
// Compute the blocks by starting a breadth-first search.
199215
while let Some(block_hash) = queue.pop_front() {
200216
if !seen.contains(block_hash) {
201217
// Retrieve the block from the cache.
202-
match state.get_block(block_hash) {
203-
Some(block) => {
204-
let block_size = block.total_size();
205-
if response_block_size == 0
206-
|| (response_block_size + block_size <= max_blocks_size
207-
&& successor_blocks.len() < MAX_BLOCKS_LENGTH
208-
&& allow_multiple_blocks)
209-
{
210-
successor_blocks.push(block.clone());
211-
response_block_size += block_size;
212-
} else {
213-
break;
214-
}
215-
}
216-
None => {
217-
// Cache miss has occurred. This block or any of its successors cannot
218-
// be returned. Discarding this subtree from the BFS.
219-
continue;
220-
}
218+
let Some(block) = state.get_block(block_hash) else {
219+
// If the block is not in the cache, we skip it and all its subtree.
220+
// We don't want to return orphaned blocks to the canister.
221+
continue;
222+
};
223+
let block_size = block.total_size();
224+
// If we have at least one block in the response, and we can't fit another block, we stop.
225+
if response_block_size > 0
226+
&& (response_block_size + block_size > max_blocks_size
227+
|| successor_blocks.len() + 1 > max_blocks_length)
228+
{
229+
break;
221230
}
231+
successor_blocks.push(block);
232+
response_block_size += block_size;
222233
}
223234

224235
queue.extend(

rs/bitcoin/adapter/tests/adapter_test.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -830,19 +830,19 @@ fn test_receives_blocks_from_forks() {
830830
.get_new_address(None, None)
831831
.unwrap()
832832
.assume_checked();
833-
client1.generate_to_address(25, &address1).unwrap();
833+
client1.generate_to_address(10, &address1).unwrap();
834834

835-
wait_for_blocks(&client1, 25);
836-
wait_for_blocks(&client2, 25);
835+
wait_for_blocks(&client1, 10);
836+
wait_for_blocks(&client2, 10);
837837

838838
let address2 = client2
839839
.get_new_address(None, None)
840840
.unwrap()
841841
.assume_checked();
842-
client2.generate_to_address(25, &address2).unwrap();
842+
client2.generate_to_address(10, &address2).unwrap();
843843

844-
wait_for_blocks(&client1, 50);
845-
wait_for_blocks(&client2, 50);
844+
wait_for_blocks(&client1, 20);
845+
wait_for_blocks(&client2, 20);
846846

847847
// Disconnect the nodes to create a fork
848848
client1
@@ -852,15 +852,15 @@ fn test_receives_blocks_from_forks() {
852852
wait_for_connection(&client1, 1);
853853
wait_for_connection(&client2, 1);
854854

855-
client1.generate_to_address(10, &address1).unwrap();
856-
client2.generate_to_address(15, &address2).unwrap();
855+
client1.generate_to_address(3, &address1).unwrap();
856+
client2.generate_to_address(6, &address2).unwrap();
857857

858-
wait_for_blocks(&client1, 60);
859-
wait_for_blocks(&client2, 65);
858+
wait_for_blocks(&client1, 23);
859+
wait_for_blocks(&client2, 26);
860860

861861
let anchor = client1.get_block_hash(0).unwrap()[..].to_vec();
862-
let blocks = sync_blocks(&adapter_client, &mut vec![], anchor, 75, 200);
863-
assert_eq!(blocks.len(), 75);
862+
let blocks = sync_blocks(&adapter_client, &mut vec![], anchor, 29, 201);
863+
assert_eq!(blocks.len(), 29);
864864
}
865865

866866
/// Checks that the adapter returns blocks in BFS order.

0 commit comments

Comments
 (0)