Skip to content

Conversation

Sjors
Copy link
Member

@Sjors Sjors commented Nov 15, 2024

It's useful to be able to wait for an active chaintip to appear, by calling waitTipChanged(uint256::ZERO);.

Unfortunately this doesn't work out of the box, because notifications().m_tip_block is ZERO until a new block arrives.

Additionally we need an early return before calling wait_for.

@DrahtBot
Copy link
Contributor

DrahtBot commented Nov 15, 2024

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31297.

Reviews

See the guideline for information on the review process.
A summary of reviews will appear here.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #31325 (Make m_tip_block std::optional by Sjors)
  • #31283 (Add waitNext() to BlockTemplate interface by Sjors)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@Sjors
Copy link
Member Author

Sjors commented Nov 15, 2024

cc @TheCharlatan life would be siugnifancly better if notifications().m_tip_block just jumped to the tip as soon as there is one, rather than wait for the next block to come in. That applies to #31283 too.

return BlockRef{tip_hash, chainman().ActiveChain().Tip()->nHeight};
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it is clearer to make m_tip_block nullopt, instead of a magic value? Would be nice to see the diff and pick the better one.

@ryanofsky
Copy link
Contributor

It's useful to be able to wait for an active chaintip to appear, by calling waitTipChanged(uint256::ZERO);.

I think the bug here is just that blockTip() is not called on startup so m_tip_block is not set when node starts up, only after it starts up and then a new block get connected. Otherwise the waitTipChanged method is already written to provide desired behavior by waiting until there is any new tip and the new tip is not zero:

            notifications().m_tip_block_cv.wait_for(lock, timeout, [&]() EXCLUSIVE_LOCKS_REQUIRED(notifications().m_tip_block_mutex) {
                return (notifications().m_tip_block != current_tip && notifications().m_tip_block != uint256::ZERO) || chainman().m_interrupt;
            });

I don't actually see how the early return if (current_tip != uint256::ZERO || current_tip != tip_hash) suggested in 435934f helps because that will be trivially true unless current_tip and tip_hash are both 0. But maybe if it said (tip_hash != uint256::ZERO && current_tip != tip_hash) that could be a partial workaround. It would still wait too long if waitTipChanged were called really early before the chain tip was loaded, but would work well as long as waitTipChanged were called after that point.

But It would seem cleaner to just call blockTip() when the tip is first loaded so no workaround is needed.

@Sjors
Copy link
Member Author

Sjors commented Nov 15, 2024

that will be trivially true unless current_tip and tip_hash are both 0

That's how I use it in Sjors#49 when the Template Provider spins up its main thread:

void Sv2TemplateProvider::ThreadSv2Handler()
{
    // Wait for the node chainstate to be ready if needed.
    auto tip{m_mining.waitTipChanged(uint256::ZERO)};
    Assert(tip.hash != uint256::ZERO);

Update 2024-11-18: just noticed || instead of &&

But It would seem cleaner to just call blockTip() when the tip is first loaded so no workaround is needed.

I tend to agree.

{
LOCK(::cs_main);
const uint256 tip_hash{chainman().ActiveChain().Tip()->GetBlockHash()};
if (current_tip != uint256::ZERO || current_tip != tip_hash) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

435934f oops, that should have been &&

@Sjors Sjors force-pushed the 2024/11/waittipchanged branch from 435934f to 71a539c Compare November 18, 2024 11:14
@Sjors
Copy link
Member Author

Sjors commented Nov 18, 2024

Pushed a bug fix, but still need to address the main feedback.

@maflcko
Copy link
Member

maflcko commented Nov 18, 2024

Also, it would be good to add test coverage?

@Sjors
Copy link
Member Author

Sjors commented Nov 18, 2024

@maflcko it's implicitly tested by the shutdown test, which is how I found this bug. Not sure how to explicitly test an early return? I could add a comment to the shutdown test that the ['active_commands']) == 2 condition will fail if waitfornewblock returned prematurely.

@maflcko
Copy link
Member

maflcko commented Nov 18, 2024

Are you sure it is covered, because the code is reported as uncovered in https://corecheck.dev/bitcoin/bitcoin/pulls/31297?

@Sjors
Copy link
Member Author

Sjors commented Nov 18, 2024

@maflcko feature_shutdown.py only checks that the early return is not used. It makes sense to test the scenario where it is used too.

@Sjors
Copy link
Member Author

Sjors commented Nov 19, 2024

I added a commit to make m_tip_block and std::optional.

Also, looking at m_tip_block again, I'm fairly sure it's set to the tip during node initialization. Otherwise we'd never get past this point:

bitcoin/src/init.cpp

Lines 1794 to 1806 in 746f93b

// Wait for genesis block to be processed
if (WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip() == nullptr)) {
WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock);
kernel_notifications.m_tip_block_cv.wait(lock, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
return !kernel_notifications.m_tip_block.IsNull() || ShutdownRequested(node);
});
}
if (ShutdownRequested(node)) {
return false;
}
// ********************************************************* Step 12: start node

So I was probably doing something wrong earlier.

I dropped the workaround. An early return only happens if:

  1. m_tip_block is set; AND
  2. m_tip_block.value() is different from current_block

If a caller sets current_tip to the special value ZERO very early in the node startup process, it will never match m_tip_block.value(), so it won't return early.

I'm still not sure how to add a functional test for this.

@Sjors Sjors force-pushed the 2024/11/waittipchanged branch from 71a539c to 8a15e46 Compare November 19, 2024 13:30
return (notifications().m_tip_block && notifications().m_tip_block.value() != current_tip) || chainman().m_interrupt;
});
// Early return if a tip is connected and already different.
if (!notifications().m_tip_block || notifications().m_tip_block.value() == current_tip) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "mining: add early return to waitTipChanged()" (71a539c)

Does this change have any effect? It looks like this is basically changing cv.wait_for(condition) to if (!condition) cv.wait_for(condition) which I would expect to be equivalent.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming wait_for won't be triggered until m_tip_block is set or modified, or the timeout passes. So it doesn't return early.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words:

if !tip_changed
  wait_for: tip_changed
return new_tip

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you are seeing a difference in practice? At least according to https://en.cppreference.com/w/cpp/thread/condition_variable/wait_for, wait_for(lock, rel_time, pred) is equivalent to wait_until(lock, rel_time + now, pred) and according to https://en.cppreference.com/w/cpp/thread/condition_variable/wait_until that should be equivalent to while(!pred()) wait... checking the predicate before there is any waiting.

Copy link
Member Author

@Sjors Sjors Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I did, but given what the documentation says, it indeed shouldn't be needed. Will test again.

@ryanofsky
Copy link
Contributor

ryanofsky commented Nov 19, 2024

re: #31297 (comment)

Otherwise we'd never get past this point:

I think we typically get past that point on line 1795 in AppInitMain even though m_tip_block is still null at that point, because when the node restarted the chainman.ActiveTip() == nullptr check will normally be false. This is because of the InitAndLoadChainstate call on line 1624, which calls Chainstate::LoadChainTip() and sets the active tip to coins_cache.GetBestBlock().

I think maybe a good fix for this issue could be to call GetNotifications().blockTip() in LoadChainTip() or maybe to just set m_tip_block directly.

It would be nice if we could have a functional test to reproduce the exact problem, but that is hard as long as python code doesn't have an easy way to call the waitTipChanged() method. It would probably be easier to test this after #30437 we have a bitcoin-mine executable that python code could run, and could test various mining methods.

@Sjors Sjors marked this pull request as draft November 19, 2024 18:44
@Sjors
Copy link
Member Author

Sjors commented Nov 19, 2024

It's a bit tricky at the moment to test the interaction of multiple pull requests on my stratum v2 branches. I'll test this again later, and most likely close the PR then. See #31297 (comment)

Meanwhile I extracted the make m_tip_block std::optional commit into #31325 for separate consideration.

@Sjors
Copy link
Member Author

Sjors commented Nov 22, 2024

I tested that m_tip_block_cv.wait_for indeed immediately checks the condition before waiting. And I could see that notifications().m_tip_block isn't set when you start a node that is already fully caught up.

I'll open a new PR to try and deal with that.

@Sjors
Copy link
Member Author

Sjors commented Nov 22, 2024

See #31346.

ryanofsky added a commit that referenced this pull request Dec 13, 2024
37946c0 Set notifications m_tip_block in LoadChainTip() (Sjors Provoost)

Pull request description:

  Ensure KernelNotifications `m_tip_block` is set even if no new block arrives.

  Suggested in #31297 (comment)

ACKs for top commit:
  ryanofsky:
    Code review ACK 37946c0, fixing comment bug caught by @mzumsande in #31346 (comment) in another really helpful clarification
  mzumsande:
    Code Review ACK 37946c0
  TheCharlatan:
    ACK 37946c0

Tree-SHA512: 931bf820440a0cdda276f6dbd63f03fdbcdc90b18e7d5e160a74bdd9d0290acc706c35aab15bbdcd6e5e0b77565b3d07ff49b0dcf6551cb83961bae67be5d1bb
ryanofsky added a commit that referenced this pull request Dec 19, 2024
81cea5d Ensure m_tip_block is never ZERO (Sjors Provoost)
e058544 Make m_tip_block an std::optional (Sjors Provoost)

Pull request description:

  Suggested in #31297 (comment)

ACKs for top commit:
  fjahr:
    re-ACK 81cea5d
  tdb3:
    code review re ACK 81cea5d
  l0rinc:
    ACK 81cea5d

Tree-SHA512: 31a75ba29e3d567bab32e4e7925a419d9d7a4d2d85ed1c1012116d8d22adc14d31d5b4ce5f6c499c994188dcd26a01cced05be74f94c892fc90ae17a6783a472
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants