Repository: bitcoin/bitcoin
File: src/validation.cpp (C++)

Code before:

    BlockValidationState state;
    bool ret;

    if (coinstip_size > old_coinstip_size) {
        // Likely no need to flush if cache sizes have grown.
        ret = FlushStateToDisk(state, FlushStateMode::IF_NEEDED);
    } else {
        // Otherwise, flush state to disk and deallocate the in-memory coins map.
        ret = FlushStateToDisk(state, FlushStateMode::ALWAYS);
    }
    return ret;
}

//! Guess how far we are in the verification process at the given block index
//! require cs_main if pindex has not been validated yet (because m_chain_tx_count might be unset)
double ChainstateManager::GuessVerificationProgress(const CBlockIndex* pindex) const
{
    const ChainTxData& data{GetParams().TxData()};
    if (pindex == nullptr) {
        return 0.0;
    }

    if (pindex->m_chain_tx_count == 0) {
        LogDebug(BCLog::VALIDATION, "Block %d has unset m_chain_tx_count. Unable to estimate verification progress.\n", pindex->nHeight);
        return 0.0;
    }

    int64_t nNow = time(nullptr);

    double fTxTotal;

    if (pindex->m_chain_tx_count <= data.tx_count) {
        fTxTotal = data.tx_count + (nNow - data.nTime) * data.dTxRate;
    } else {
        fTxTotal = pindex->m_chain_tx_count + (nNow - pindex->GetBlockTime()) * data.dTxRate;
    }

    return std::min<double>(pindex->m_chain_tx_count / fTxTotal, 1.0);
}

std::optional<uint256> ChainstateManager::SnapshotBlockhash() const
{
    LOCK(::cs_main);
    if (m_active_chainstate && m_active_chainstate->m_from_snapshot_blockhash) {
        // If a snapshot chainstate exists, it will always be our active.
        return m_active_chainstate->m_from_snapshot_blockhash;
    }
    return std::nullopt;
}


Review comment (performance):
> In case we know there are unprocessed blocks in the chain (because we have their headers), we shouldn't say progress: 1

I agree we should not return 1 if we didn't verify the last block we know (we don't want Core to lie to users 😉)

What if we do something like:
```c++
const auto block_time{(pindex->nHeight == m_best_header->nHeight && pindex->nChainWork == m_best_header->nChainWork) 
    ? pindex->GetBlockTime() + Ticks<std::chrono::seconds>(2h) 
    : pindex->GetBlockTime()};
```

So we return 1 if we are in the best known header and inside the two hours offset.

```
2025-05-19T22:32:55Z UpdateTip: new best=000000000000000000007de21c2c10adec3376d6f0c69f24ba143dc572497b87 height=897462 version=0x202c2000 log2_work=95.617541 tx=1192584903 date='2025-05-19T22:05:52Z' progress=0.999994 cache=111.4MiB(825445txo)
2025-05-19T22:32:55Z UpdateTip: new best=00000000000000000001efdca54c71c76ae3c06b54598ff50fc20b48a01149ab height=897463 version=0x292e4000 log2_work=95.617554 tx=1192586948 date='2025-05-19T22:07:05Z' progress=1.000000 cache=111.9MiB(829042txo)
```
```
$ bitcoin-cli getblockchaininfo
  "chain": "main",
  "blocks": 897463,
  "headers": 897463,
....
  "verificationprogress": 1,
...
```

Code after:

    BlockValidationState state;
    bool ret;

    if (coinstip_size > old_coinstip_size) {
        // Likely no need to flush if cache sizes have grown.
        ret = FlushStateToDisk(state, FlushStateMode::IF_NEEDED);
    } else {
        // Otherwise, flush state to disk and deallocate the in-memory coins map.
        ret = FlushStateToDisk(state, FlushStateMode::ALWAYS);
    }
    return ret;
}

double ChainstateManager::GuessVerificationProgress(const CBlockIndex* pindex) const
{
    AssertLockHeld(GetMutex());
    const ChainTxData& data{GetParams().TxData()};
    if (pindex == nullptr) {
        return 0.0;
    }

    if (pindex->m_chain_tx_count == 0) {
        LogDebug(BCLog::VALIDATION, "Block %d has unset m_chain_tx_count. Unable to estimate verification progress.\n", pindex->nHeight);
        return 0.0;
    }

    const int64_t nNow{TicksSinceEpoch<std::chrono::seconds>(NodeClock::now())};
    const auto block_time{
        (Assume(m_best_header) && std::abs(nNow - pindex->GetBlockTime()) <= Ticks<std::chrono::seconds>(2h) &&
         Assume(m_best_header->nHeight >= pindex->nHeight)) ?
            // When the header is known to be recent, switch to a height-based
            // approach. This ensures the returned value is quantized when
            // close to "1.0", because some users expect it to be. This also
            // avoids relying too much on the exact miner-set timestamp, which
            // may be off.
            nNow - (m_best_header->nHeight - pindex->nHeight) * GetConsensus().nPowTargetSpacing :
            pindex->GetBlockTime(),
    };

    double fTxTotal;

    if (pindex->m_chain_tx_count <= data.tx_count) {
        fTxTotal = data.tx_count + (nNow - data.nTime) * data.dTxRate;
    } else {
        fTxTotal = pindex->m_chain_tx_count + (nNow - block_time) * data.dTxRate;
    }

    return std::min<double>(pindex->m_chain_tx_count / fTxTotal, 1.0);
}