diff --git a/zebra-consensus/src/checkpoint.rs b/zebra-consensus/src/checkpoint.rs index c93e3ad53..9d5816a5b 100644 --- a/zebra-consensus/src/checkpoint.rs +++ b/zebra-consensus/src/checkpoint.rs @@ -56,10 +56,24 @@ struct QueuedBlock { /// A list of unverified blocks at a particular height. /// -/// Typically contains zero or one blocks, but might contain more if a peer +/// Typically contains a single block, but might contain more if a peer /// has an old chain fork. (Or sends us a bad block.) +/// +/// The CheckpointVerifier avoids creating zero-block lists. type QueuedBlockList = Vec; +/// The maximum number of queued blocks at any one height. +/// +/// This value is a tradeoff between: +/// - rejecting bad blocks: if we queue more blocks, we need fewer network +/// retries, but use a bit more CPU when verifying, +/// - avoiding a memory DoS: if we queue fewer blocks, we use less memory. +/// +/// Memory usage is controlled by the sync service, because it controls block +/// downloads. When the verifier services process blocks, they reduce memory +/// usage by committing blocks to the disk state. (Or dropping invalid blocks.) +pub const MAX_QUEUED_BLOCKS_PER_HEIGHT: usize = 4; + /// A checkpointing block verifier. /// /// Verifies blocks using a supplied list of checkpoints. There must be at @@ -297,10 +311,26 @@ impl CheckpointVerifier { } }; + // Since we're using Arc, each entry is a single pointer to the + // Arc. But there are a lot of QueuedBlockLists in the queue, so we keep + // allocations as small as possible. + let qblocks = self + .queued + .entry(height) + .or_insert_with(|| QueuedBlockList::with_capacity(1)); + + // Memory DoS resistance: limit the queued blocks at each height + if qblocks.len() >= MAX_QUEUED_BLOCKS_PER_HEIGHT { + let _ = tx.send(Err("too many queued blocks at this height".into())); + return rx; + } + // Add the block to the list of queued blocks at this height let hash = block.as_ref().into(); let new_qblock = QueuedBlock { block, hash, tx }; - self.queued.entry(height).or_default().push(new_qblock); + // This is a no-op for the first block in each QueuedBlockList. + qblocks.reserve_exact(1); + qblocks.push(new_qblock); rx } diff --git a/zebrad/src/commands/start/sync.rs b/zebrad/src/commands/start/sync.rs index 447aa543b..f0ad32d9b 100644 --- a/zebrad/src/commands/start/sync.rs +++ b/zebrad/src/commands/start/sync.rs @@ -10,6 +10,7 @@ use zebra_chain::{ block::{Block, BlockHeaderHash}, types::BlockHeight, }; +use zebra_consensus::checkpoint; use zebra_network::{self as zn, RetryLimit}; use zebra_state::{self as zs}; @@ -38,7 +39,9 @@ where verifier, retry_peer_set, block_requests: FuturesUnordered::new(), - fanout: 4, + // Limit the fanout to the number of chains that the + // CheckpointVerifier can handle + fanout: checkpoint::MAX_QUEUED_BLOCKS_PER_HEIGHT, prospective_tips: HashSet::new(), } } @@ -338,4 +341,4 @@ pub fn block_locator_heights(tip_height: BlockHeight) -> impl Iterator; -type NumReq = u32; +type NumReq = usize;