Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions libraries/chain/include/eosio/chain/block_handle.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,59 @@ struct block_handle {

void write(const std::filesystem::path& state_file);
bool read(const std::filesystem::path& state_file);

// Returns true if this block carries a strong QC for a block that is not
// in `head_handle`'s ancestry.
//
// Under Savanna a strong QC for some block B implies that at least 2/3 of
// finalizer weight voted strong on B. The safety rule for strong votes
// locks those finalizers on B, so they cannot subsequently vote in a way
// that would let any branch not extending B form its own QC. Therefore if
// `head_handle` is on a branch that does not include the QC target, no
// block built on `head_handle` can ever be covered by a future QC, and
// `head_handle`'s branch cannot win fork-choice -- it is permanently
// locked out.
//
// Returns false in legacy (non-Savanna) mode and when no strong QC is
// present.
//
// Thread-safety: safe to call concurrently with block production / apply.
// `block_state_ptr` is a shared_ptr (its copy is atomic) and the
// `finality_core` it references is immutable after construction. The
// accessors used (`latest_qc_claim`, `get_block_reference`, `extends`)
// are const reads against that immutable state.
bool locks_out_branch_of(const block_handle& head_handle) const {
if (!std::holds_alternative<block_state_ptr>(_bsp) ||
!std::holds_alternative<block_state_ptr>(head_handle._bsp))
return false;

const auto& bsp = std::get<block_state_ptr>(_bsp);
const auto& head_bsp = std::get<block_state_ptr>(head_handle._bsp);

const auto qc = bsp->core.latest_qc_claim();
if (!qc.is_strong_qc)
return false;

const auto& this_id = bsp->id();
const auto& head_id = head_bsp->id();

// If head is on this block's branch (head is this block, or this block extends head),
// they share the QC's chain -- head's branch can produce blocks that include the QC
// target as an ancestor. Not locked out.
if (head_id == this_id || bsp->core.extends(head_id))
return false;

// If the QC target is in head's ancestry (or is head itself), head's branch already
// includes the block the QC was formed for. Not locked out.
const auto& qc_target_id = bsp->core.get_block_reference(qc.block_num).block_id;
if (head_id == qc_target_id || head_bsp->core.extends(qc_target_id))
return false;

// Otherwise head's branch and the QC target are on incompatible branches: any block
// built on head conflicts with the QC target, and no QC can ever be formed on head's
// branch. Locked out.
return true;
}
};

} // namespace eosio::chain
Expand Down
19 changes: 13 additions & 6 deletions plugins/producer_plugin/producer_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -938,16 +938,23 @@ class producer_plugin_impl : public std::enable_shared_from_this<producer_plugin

auto& chain = chain_plug->chain();

// While producing our own block, normally defer applying incoming blocks to avoid disrupting
// production mid-block. Exception: if the fork-database best head carries a strong QC for a
// block not in our applied head's ancestry, our head's branch can no longer form a QC that
// wins fork-choice -- continuing to produce on it is pointless and the resulting blocks would
// be orphaned at the next fork switch. In that case fall through and apply blocks now.
if (in_producing_mode()) {
if (_log.is_enabled(fc::log_level::info)) {
auto fhead = chain.fork_db_head();
const block_handle fhead = chain.fork_db_head();
if (!fhead.locks_out_branch_of(chain.head())) {
fc_ilog(_log, "producing, fork database head at: #${num} id: ${id}",
("num", fhead.block_num())("id", fhead.id()));
_time_tracker.add_other_time();
// return complete as we are producing and don't want to be interrupted right now. Next start_block will
// give an opportunity for this incoming block to be processed.
return {};
}
_time_tracker.add_other_time();
// return complete as we are producing and don't want to be interrupted right now. Next start_block will
// give an opportunity for this incoming block to be processed.
return {};
fc_ilog(_log, "applying blocks while producing: head's branch is locked out of fork-choice by a strong QC at fork-db head #${num} ${id}",
("num", fhead.block_num())("id", fhead.id()));
}

// no reason to abort_block if we have nothing ready to process
Expand Down
12 changes: 12 additions & 0 deletions tests/nodeos_late_block_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@
Print("Verify fork switch")
assert node3.findInLog("switching forks .* defproducerk"), "Expected to find 'switching forks' in node_03 log"

# Verify the lockout-detection optimization fired: when the bridge reconnects, node_03
# is still inside its producing slot, and the rest of the network's blocks (carrying a
# strong QC for a block on the canonical branch) reach node_03's fork database. The
# producer plugin's in_producing_mode early-return should fall through and apply blocks
# immediately rather than waiting for the slot to end. Without this optimization,
# node_03 would orphan its entire round.
def findApplyDuringProducing():
return node3.findInLog("applying blocks while producing: head's branch is locked out")
applyDuringProducingLine = Utils.waitForBool(findApplyDuringProducing, timeout=30)
assert applyDuringProducingLine, \
"Expected node_03 to apply blocks mid-slot upon detecting strong-QC lockout of its isolated fork"

Print("Wait until Node_00 to produce")
node3.waitForProducer("defproducera")

Expand Down
76 changes: 76 additions & 0 deletions unittests/fork_db_tests.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <eosio/chain/types.hpp>
#include <eosio/chain/fork_database.hpp>
#include <eosio/chain/block_handle.hpp>
#include <eosio/testing/tester.hpp>
#include <fc/bitutil.hpp>
#include <boost/test/unit_test.hpp>
Expand Down Expand Up @@ -249,4 +250,79 @@ BOOST_FIXTURE_TEST_CASE(is_child_of, generate_fork_db_state) try {

} FC_LOG_AND_RETHROW();

// Tests for block_handle::locks_out_branch_of()
// ----------------------------------------------
// Build two branches of equal length sharing a common root, attach blocks with
// specific QC claims (strong on a non-shared block, strong on a shared block,
// weak), and verify lockout is reported only when the strong-QC's target is
// not in the candidate head's ancestry and the head is not on the QC carrier's
// branch.
BOOST_AUTO_TEST_CASE(locks_out_branch_of_test) try {
nonce = 0;

// Helper that overlays a custom qc_claim on a freshly-created block_state.
auto make_block_with_qc = [](block_num_type block_num, const block_state_ptr& prev,
const qc_claim_t& qc) {
auto bsp = test_block_state_accessor::make_unique_block_state(block_num, prev);
bsp->core = prev->core.next(prev->make_block_ref(), qc);
return bsp;
};

// root (block 10)
// / \
// bsp11a bsp11b
// | |
// bsp12a bsp12b
// |
// { bsp13a_strong, bsp13a_weak, bsp13a_shared } (three variants of block 13 on branch A,
// differing only in their carried qc_claim)
// and
// bsp13b_strong (on branch B, strong QC for bsp12b)

block_state_ptr root = test_block_state_accessor::make_genesis_block_state();
block_state_ptr bsp11a = test_block_state_accessor::make_unique_block_state(11, root);
block_state_ptr bsp12a = test_block_state_accessor::make_unique_block_state(12, bsp11a);
block_state_ptr bsp11b = test_block_state_accessor::make_unique_block_state(11, root);
block_state_ptr bsp12b = test_block_state_accessor::make_unique_block_state(12, bsp11b);

block_state_ptr bsp13a_strong = make_block_with_qc(13, bsp12a, {.block_num = 12, .is_strong_qc = true});
block_state_ptr bsp13a_weak = make_block_with_qc(13, bsp12a, {.block_num = 12, .is_strong_qc = false});
block_state_ptr bsp13a_shared = make_block_with_qc(13, bsp12a, {.block_num = 10, .is_strong_qc = true});
block_state_ptr bsp13b_strong = make_block_with_qc(13, bsp12b, {.block_num = 12, .is_strong_qc = true});

block_handle h13a_strong{bsp13a_strong};
block_handle h13a_weak{bsp13a_weak};
block_handle h13a_shared{bsp13a_shared};
block_handle h13b_strong{bsp13b_strong};
block_handle h12a{bsp12a};
block_handle h12b{bsp12b};
block_handle h11a{bsp11a};
block_handle h11b{bsp11b};
block_handle h_root{root};

// Strong QC for a block on a different branch from `head` -> head locked out.
BOOST_TEST(h13a_strong.locks_out_branch_of(h12b));
BOOST_TEST(h13a_strong.locks_out_branch_of(h11b));
BOOST_TEST(h13b_strong.locks_out_branch_of(h12a));
BOOST_TEST(h13b_strong.locks_out_branch_of(h11a));

// Strong QC for a block on the same branch as head -> not locked out.
// h12a is the QC target itself; h11a is an ancestor of the QC target.
BOOST_TEST(!h13a_strong.locks_out_branch_of(h12a));
BOOST_TEST(!h13a_strong.locks_out_branch_of(h11a));

// Strong QC for a shared ancestor (the genesis root) -> not locked out for either branch.
BOOST_TEST(!h13a_shared.locks_out_branch_of(h12b));
BOOST_TEST(!h13a_shared.locks_out_branch_of(h11b));
BOOST_TEST(!h13a_shared.locks_out_branch_of(h_root));

// Head identical to the new block -> not locked out.
BOOST_TEST(!h13a_strong.locks_out_branch_of(h13a_strong));

// Weak QC, even on a non-shared block -> not locked out (weak votes don't lock finalizers).
BOOST_TEST(!h13a_weak.locks_out_branch_of(h12b));
BOOST_TEST(!h13a_weak.locks_out_branch_of(h11b));

} FC_LOG_AND_RETHROW();

BOOST_AUTO_TEST_SUITE_END()
Loading