View in context in the libbitcoin Slack #general mirror.
a has a bad sig. when validating `a` the bad signature row is written into the batch table under the batch mutex. But a’s block id is not added to batched_ at that moment, rather it is posted later to the strand via push_batch().
before push_batch() runs, block b finishes and triggers batch verification. verifier sees a’s bad signature row, correctly marks a unconfirmable, and purges the table. but because a’s id is not yet in batched_, the invalid link cleanup cannot mark a’s entry as terminal.
now a’s delayed push_batch() finally runs and appends a to batched_. On the next batch, process_valids() treats every non-terminal link in batched_ as valid, so a can be marked valid even though it was already marked unconfirmable for an invalid signature.
relevant code paths (not using line links because i also have some local changes):
// chaser_validate_parallel.cpp
{
std::shared_lock lock{ mutex_, std::defer_lock };
if (capturing) lock.lock();
if ((ec = block.connect(ctx, capture)))
return ec;
batched = capture.batched;
faulted = capture.faulted;
}// chaser_validate.cpp
if (batched)
{
++batch_backlog_;
POST(push_batch, link, height);
return;
}// chaser_validate_batch.cpp batched_.push_back(link); process_batch(is_maximum());
// chaser_validate_batch.cpp
for (const auto& link: invalids)
{
if (!query.get_height(height, link) ||
!query.set_block_unconfirmable(link))
return false;
notify_block(system::error::invalid_signature, height, link, false);
}
std::ranges::replace_if(batched_, [&](const auto& link) NOEXCEPT
{
return contains(invalids, link);
}, header_link::terminal);// chaser_validate_batch.cpp
for (const auto& link: batched_)
{
if (link == header_link::terminal)
continue;
if (!query.get_height(height, link) ||
!query.set_block_valid(link))
return false;
notify_block(system::error::success, height, link, false);
}