From aaf507c142a23ffc537e21c6aaa387f4736deb4e Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 4 Aug 2026 15:47:28 -0400 Subject: [PATCH 1/3] Modest DoS improvement to header accumulator. --- .../bitcoin/node/chasers/chaser_organize.hpp | 15 ++++ .../node/impl/chasers/chaser_organize.ipp | 88 ++++++++++++++++--- 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_organize.hpp b/include/bitcoin/node/chasers/chaser_organize.hpp index 2b50e21d..8287f5b0 100644 --- a/include/bitcoin/node/chasers/chaser_organize.hpp +++ b/include/bitcoin/node/chasers/chaser_organize.hpp @@ -163,6 +163,19 @@ class chaser_organize void cache(const typename Block::cptr& block, const chain_state::cptr& state) NOEXCEPT; + // Checkpoint gate. + // ------------------------------------------------------------------------ + + // True if the parent of a new block is below the top reached checkpoint. + bool is_under_active_checkpoint( + const system::hash_digest& previous) const NOEXCEPT; + + // Advance top reached checkpoint and purge the tree at/below it. + void update_checkpoint(height_t top) NOEXCEPT; + + // Remove tree blocks at/below the top reached checkpoint (conflicted). + void purge_under_checkpoint() NOEXCEPT; + // Getters. // ------------------------------------------------------------------------ @@ -188,6 +201,8 @@ class chaser_organize // These are protected by strand. bool bumped_{}; + size_t next_checkpoint_{}; + size_t active_checkpoint_{}; chain_state::cptr state_{}; // TODO: optimize, default bucket count is around 8. diff --git a/include/bitcoin/node/impl/chasers/chaser_organize.ipp b/include/bitcoin/node/impl/chasers/chaser_organize.ipp index 34590401..d52642a2 100644 --- a/include/bitcoin/node/impl/chasers/chaser_organize.ipp +++ b/include/bitcoin/node/impl/chasers/chaser_organize.ipp @@ -61,6 +61,7 @@ code CLASS::start() NOEXCEPT LOGN("Candidate top [" << system::encode_hash(state_->hash()) << ":" << state_->height() << "]."); + update_checkpoint(top); SUBSCRIBE_CHASE(handle_chase, _1, _2, _3); return error::success; } @@ -156,6 +157,13 @@ void CLASS::do_organize(typename Block::cptr block, return; } + // Shortcircuit fork at/under the top reached checkpoint. + if (is_under_active_checkpoint(previous)) + { + handler(system::error::checkpoint_conflict, {}); + return; + } + // Obtain parent state from state_, tree, or store as applicable. const auto parent = get_chain_state(previous); if (!parent) @@ -177,15 +185,6 @@ void CLASS::do_organize(typename Block::cptr block, return; }; - // TODO: If any checkpoint is reached then reject non-candidates below. - // TODO: because checkpoints are storable (and therefore stored) along with - // TODO: all ancestor blocks, which therefore must be candidates as well. - // TODO: When a checkpoint is pushed and after its branch is reorganized, - // TODO: purge all blocks in the tree with height at/below that checkpoint. - // TODO: The combination strongly mitigates low pow sybil attacks against - // TODO: the header tree, as all are purged as each checkpoint is reached, - // TODO: and no more are ever accepted below the top checkpoint. - // Blocks of headers are validated later, malleations ignored until then. // Blocks are fully validated (not confirmed), so malleation is non-issue. if (const auto ec = validate(*block, *state)) @@ -324,6 +323,9 @@ void CLASS::do_organize(typename Block::cptr block, // Logs from candidate block parent to the candidate (forward sequential). log_state_change(*parent, *state); state_ = state; + + // Advance top reached checkpoint and purge the tree at/below it. + update_checkpoint(height); handler(error::success, height); } @@ -536,10 +538,76 @@ void CLASS::cache(const typename Block::cptr& block, // Any block obtained from the tree must have state cached. block->set_state(state); - // TODO: guard cache against memory exhaustion (DoS). tree_.emplace(block->get_hash(), block); } +TEMPLATE +bool CLASS::is_under_active_checkpoint( + const system::hash_digest& previous) const NOEXCEPT +{ + BC_ASSERT(stranded()); + const auto& query = archive(); + + if (is_zero(active_checkpoint_)) + return false; + + // Extending the candidate top (the common case, necessarily above). + if (state_->hash() == previous) + return false; + + // Tree blocks are necessarily above (purged as checkpoints are reached). + if (tree_.find(previous) != tree_.end()) + return false; + + // Unstored parent is the orphan case (handled downstream). + const auto link = query.to_header(previous); + if (link.is_terminal()) + return false; + + // The new block is a child, so at/under when its parent is under. + return query.get_height(link) < active_checkpoint_; +} + +// Set the highest checkpoint reached in the candidate chain. +TEMPLATE +void CLASS::update_checkpoint(height_t top) NOEXCEPT +{ + if (top < next_checkpoint_) + return; + + next_checkpoint_ = max_size_t; + const auto previous = active_checkpoint_; + for (const auto& item: checkpoints_) + { + if (item.height() <= top) + active_checkpoint_ = std::max(active_checkpoint_, item.height()); + else + next_checkpoint_ = std::min(next_checkpoint_, item.height()); + } + + if (active_checkpoint_ != previous) + { + LOGV("Checkpoint [" << active_checkpoint_ << "] reached."); + purge_under_checkpoint(); + } +} + +TEMPLATE +void CLASS::purge_under_checkpoint() NOEXCEPT +{ + // Purged blocks conflict with the reached checkpoint (dead branches). + const auto count = std::erase_if(tree_, [this](const auto& entry) NOEXCEPT + { + return entry.second->get_state()->height() <= active_checkpoint_; + }); + + if (!is_zero(count)) + { + LOGN("Purged (" << count << ") blocks under checkpoint [" + << active_checkpoint_ << "]."); + } +} + // Private getters // ---------------------------------------------------------------------------- From 63c9b1478b26dc4e8005b1da2a9cdeb1f2a3a236 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 4 Aug 2026 16:03:26 -0400 Subject: [PATCH 2/3] Style, comments. --- include/bitcoin/node/impl/chasers/chaser_organize.ipp | 4 ++-- src/chasers/chaser_header.cpp | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/include/bitcoin/node/impl/chasers/chaser_organize.ipp b/include/bitcoin/node/impl/chasers/chaser_organize.ipp index d52642a2..eb487086 100644 --- a/include/bitcoin/node/impl/chasers/chaser_organize.ipp +++ b/include/bitcoin/node/impl/chasers/chaser_organize.ipp @@ -183,7 +183,7 @@ void CLASS::do_organize(typename Block::cptr block, { handler(system::error::checkpoint_conflict, height); return; - }; + } // Blocks of headers are validated later, malleations ignored until then. // Blocks are fully validated (not confirmed), so malleation is non-issue. @@ -517,7 +517,7 @@ code CLASS::push_block(const Block& block, // events::header_archived | events::block_archived fire(events_object_archived(), ctx.height); LOGV("Header archived: " << ctx.height); - return set_organized(link, ctx.height) ? ec : error::organize14; + return set_organized(link, ctx.height) ? error::success : error::organize14; } TEMPLATE diff --git a/src/chasers/chaser_header.cpp b/src/chasers/chaser_header.cpp index 05c39fde..913f5f8e 100644 --- a/src/chasers/chaser_header.cpp +++ b/src/chasers/chaser_header.cpp @@ -82,6 +82,7 @@ code chaser_header::duplicate(size_t& height, return ec; } + // height set to max_size_t unless unconfirmable. return error::duplicate_header; } From 34e92ad7c9a2927dfeda6bafac503818b897bada Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 4 Aug 2026 16:11:04 -0400 Subject: [PATCH 3/3] Release header tree hashmap once candidate chain is current. --- .../bitcoin/node/chasers/chaser_organize.hpp | 6 ++++-- .../node/impl/chasers/chaser_organize.ipp | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_organize.hpp b/include/bitcoin/node/chasers/chaser_organize.hpp index 8287f5b0..1a506649 100644 --- a/include/bitcoin/node/chasers/chaser_organize.hpp +++ b/include/bitcoin/node/chasers/chaser_organize.hpp @@ -176,6 +176,9 @@ class chaser_organize // Remove tree blocks at/below the top reached checkpoint (conflicted). void purge_under_checkpoint() NOEXCEPT; + // Release tree buckets retained from accumulation (once, when current). + void shrink_tree(bool current) NOEXCEPT; + // Getters. // ------------------------------------------------------------------------ @@ -201,11 +204,10 @@ class chaser_organize // These are protected by strand. bool bumped_{}; + bool shrunk_{}; size_t next_checkpoint_{}; size_t active_checkpoint_{}; chain_state::cptr state_{}; - - // TODO: optimize, default bucket count is around 8. block_tree tree_{}; }; diff --git a/include/bitcoin/node/impl/chasers/chaser_organize.ipp b/include/bitcoin/node/impl/chasers/chaser_organize.ipp index eb487086..ed848000 100644 --- a/include/bitcoin/node/impl/chasers/chaser_organize.ipp +++ b/include/bitcoin/node/impl/chasers/chaser_organize.ipp @@ -299,9 +299,12 @@ void CLASS::do_organize(typename Block::cptr block, // Reset top chain state and notify. // ........................................................................ + // Evaluated independently of the block short-circuit below. + const auto current = is_current_time(header.timestamp()); + // Delay so headers can get current before block download starts. // Checking currency before notify also avoids excessive work backlog. - if (is_block() || is_current_time(header.timestamp())) + if (is_block() || current) { if (!bumped_) { @@ -326,6 +329,7 @@ void CLASS::do_organize(typename Block::cptr block, // Advance top reached checkpoint and purge the tree at/below it. update_checkpoint(height); + shrink_tree(current); handler(error::success, height); } @@ -608,6 +612,18 @@ void CLASS::purge_under_checkpoint() NOEXCEPT } } +TEMPLATE +void CLASS::shrink_tree(bool current) NOEXCEPT +{ + BC_ASSERT(stranded()); + if (shrunk_ || !current) + return; + + shrunk_ = true; + tree_.rehash(zero); + LOGV("Tree buckets reduced to (" << tree_.bucket_count() << ")."); +} + // Private getters // ----------------------------------------------------------------------------