Coverage Report

Created: 2026-08-25 19:22

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/root/bitcoin/src/index/txindex.cpp
Line
Count
Source
1
// Copyright (c) 2017-present The Bitcoin Core developers
2
// Distributed under the MIT software license, see the accompanying
3
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5
#include <index/txindex.h>
6
7
#include <chain.h>
8
#include <common/args.h>
9
#include <crypto/siphash.h>
10
#include <dbwrapper.h>
11
#include <flatfile.h>
12
#include <index/base.h>
13
#include <index/disktxpos.h>
14
#include <index/txindex_key.h>
15
#include <interfaces/chain.h>
16
#include <node/blockstorage.h>
17
#include <primitives/block.h>
18
#include <primitives/transaction.h>
19
#include <random.h>
20
#include <serialize.h>
21
#include <streams.h>
22
#include <sync.h>
23
#include <uint256.h>
24
#include <util/fs.h>
25
#include <util/log.h>
26
#include <validation.h>
27
28
#include <algorithm>
29
#include <array>
30
#include <cassert>
31
#include <cstdint>
32
#include <cstdio>
33
#include <exception>
34
#include <functional>
35
#include <memory>
36
#include <optional>
37
#include <string>
38
#include <utility>
39
#include <vector>
40
41
std::unique_ptr<TxIndex> g_txindex;
42
43
namespace {
44
SipHasher13UJ ReadOrCreateTxidHasher(CDBWrapper& db)
45
0
{
46
0
    std::pair<uint64_t, uint64_t> salt;
47
0
    if (!db.Read(txindex::DB_TXID_HASH_SALT, salt)) {
  Branch (47:9): [True: 0, False: 0]
48
0
        FastRandomContext rng{};
49
0
        salt = {rng.rand64(), rng.rand64()};
50
0
        db.Write(txindex::DB_TXID_HASH_SALT, salt, /*fSync=*/true);
51
0
    }
52
0
    return SipHasher13UJ{salt.first, salt.second};
53
0
}
54
} // namespace
55
56
/** Access to the txindex database (indexes/txindex/) */
57
class TxIndex::DB : public BaseIndex::DB
58
{
59
public:
60
    explicit DB(size_t n_cache_size, bool f_memory = false, bool f_wipe = false);
61
62
    /// Write a block of transaction positions to the DB.
63
    void WriteTxs(const interfaces::BlockInfo& block);
64
65
    /// Used to hash the txid to compute the prefix.
66
    const SipHasher13UJ m_hasher;
67
68
    /// Whether the database contains any legacy ('t' + txid) entries.
69
    const bool m_has_legacy;
70
71
    CBlockLocator ReadBestBlock() const override;
72
    void WriteBestBlock(CDBBatch& batch, const CBlockLocator& locator) override;
73
74
private:
75
    DB(size_t n_cache_size, bool f_memory, bool f_wipe, bool has_legacy);
76
};
77
78
0
static fs::path TxIndexDBPath() { return gArgs.GetDataDirNet() / "indexes" / "txindex"; }
79
80
TxIndex::DB::DB(size_t n_cache_size, bool f_memory, bool f_wipe) :
81
    // Bloom filters are built for every key but only consulted by point reads,
82
    // which iterators bypass: the per-tx hashed ('x') lookups seek with an
83
    // iterator, and the 's'/'h' point reads are at most one per block against a
84
    // tiny keyspace. Only the legacy entries' per-tx point lookups benefit, so
85
    // enable the filters only for databases still containing them.
86
0
    DB(n_cache_size, f_memory, f_wipe,
87
0
       /*has_legacy=*/!f_memory && !f_wipe && CDBWrapper::HasKeyStartingWith(TxIndexDBPath(), txindex::DB_TXINDEX))
  Branch (87:23): [True: 0, False: 0]
  Branch (87:36): [True: 0, False: 0]
  Branch (87:47): [True: 0, False: 0]
88
0
{}
89
90
TxIndex::DB::DB(size_t n_cache_size, bool f_memory, bool f_wipe, bool has_legacy) :
91
0
    BaseIndex::DB(TxIndexDBPath(), n_cache_size, f_memory, f_wipe, /*f_obfuscate=*/false, /*f_bloom=*/has_legacy),
92
0
    m_hasher{ReadOrCreateTxidHasher(*this)},
93
0
    m_has_legacy{has_legacy}
94
0
{}
95
96
CBlockLocator TxIndex::DB::ReadBestBlock() const
97
0
{
98
0
    CBlockLocator locator;
99
0
    if (Read(txindex::DB_BEST_BLOCK_V2, locator)) {
  Branch (99:9): [True: 0, False: 0]
100
0
        return locator;
101
0
    }
102
    // If we don't have a locator yet, start from the legacy best block.
103
0
    return BaseIndex::DB::ReadBestBlock();
104
0
}
105
106
void TxIndex::DB::WriteBestBlock(CDBBatch& batch, const CBlockLocator& locator)
107
0
{
108
0
    batch.Write(txindex::DB_BEST_BLOCK_V2, locator);
109
0
}
110
111
void TxIndex::DB::WriteTxs(const interfaces::BlockInfo& block)
112
0
{
113
    // A block may be submitted again after it was already indexed, e.g. when it
114
    // reconnects after a reorg or is re-processed after an unclean shutdown. It
115
    // keeps its original sequence number, so skip it to avoid duplicate entries.
116
0
    if (Exists(txindex::BlockHashKey{block.hash})) return;
  Branch (116:9): [True: 0, False: 0]
117
118
0
    uint32_t block_seq{0};
119
0
    Read(txindex::DB_NEXT_BLOCK_SEQ, block_seq);
120
121
0
    CDBBatch batch(*this);
122
0
    batch.Write(txindex::BlockHashKey{block.hash}, block_seq);
123
0
    batch.Write(txindex::BlockSeqKey{block_seq}, block.hash);
124
0
    batch.Write(txindex::DB_NEXT_BLOCK_SEQ, block_seq + 1);
125
0
    uint32_t tx_offset_in_block{txindex::BLOCK_HEADER_SIZE + GetSizeOfCompactSize(block.data->vtx.size())};
126
0
    for (const auto& tx : block.data->vtx) {
  Branch (126:25): [True: 0, False: 0]
127
0
        const txindex::DBKey key{txindex::CreateKeyPrefix(m_hasher, tx->GetHash()),
128
0
                                 txindex::BlockTxPosition{block_seq, tx_offset_in_block}};
129
0
        batch.Write(key, txindex::EMPTY_VALUE);
130
0
        tx_offset_in_block += tx->ComputeTotalSize();
131
0
    }
132
0
    WriteBatch(batch);
133
0
}
134
135
TxIndex::TxIndex(std::unique_ptr<interfaces::Chain> chain, size_t n_cache_size, bool f_memory, bool f_wipe)
136
0
    : BaseIndex(std::move(chain), "txindex", "txidx"), m_db(std::make_unique<TxIndex::DB>(n_cache_size, f_memory, f_wipe))
137
0
{
138
0
    if (m_db->m_has_legacy) {
  Branch (138:9): [True: 0, False: 0]
139
0
        LogInfo("txindex contains entries in the legacy format, which uses excessive disk space. "
140
0
                "To reclaim disk space, stop the node, delete %s and restart to rebuild the index.",
141
0
                fs::PathToString(TxIndexDBPath()));
142
0
    }
143
0
}
144
145
0
TxIndex::~TxIndex() = default;
146
147
bool TxIndex::CustomAppend(const interfaces::BlockInfo& block)
148
0
{
149
    // Exclude genesis block transaction because outputs are not spendable.
150
0
    if (block.height == 0) return true;
  Branch (150:9): [True: 0, False: 0]
151
152
0
    assert(block.data);
  Branch (152:5): [True: 0, False: 0]
153
0
    m_db->WriteTxs(block);
154
0
    return true;
155
0
}
156
157
0
BaseIndex::DB& TxIndex::GetDB() const { return *m_db; }
158
159
std::optional<TxIndexResult> TxIndex::FindTx(const Txid& tx_hash) const
160
0
{
161
0
    struct Candidate {
162
0
        FlatFilePos tx_position;
163
0
        uint256 block_hash;
164
0
        uint32_t block_seq;
165
        //! Whether this candidate's block is currently in the active chain.
166
        //! Active chain candidates are attempted first, so duplicate entries
167
        //! in both active and stale blocks will always return the active block hash.
168
0
        bool in_active_chain;
169
0
    };
170
0
    std::vector<Candidate> candidates;
171
0
    {
172
0
        std::unique_ptr<CDBIterator> it{m_db->NewIterator()};
173
0
        const txindex::TxHashKeyPrefix prefix{txindex::CreateKeyPrefix(m_db->m_hasher, tx_hash)};
174
0
        txindex::DBKey key{prefix, {}};
175
0
        for (it->Seek(key); it->Valid() && it->GetKey(key) && key.hash_prefix == prefix; it->Next()) {
  Branch (175:29): [True: 0, False: 0]
  Branch (175:44): [True: 0, False: 0]
  Branch (175:63): [True: 0, False: 0]
176
0
            uint256 candidate_block_hash;
177
0
            if (!m_db->Read(txindex::BlockSeqKey{key.pos.block_seq}, candidate_block_hash)) {
  Branch (177:17): [True: 0, False: 0]
178
0
                LogWarning("Block sequence %u not found for txid %s", key.pos.block_seq, tx_hash.ToString());
179
0
                continue;
180
0
            }
181
0
            LOCK(cs_main);
182
0
            const CBlockIndex* block_index{m_chainstate->m_blockman.LookupBlockIndex(candidate_block_hash)};
183
0
            if (!block_index) {
  Branch (183:17): [True: 0, False: 0]
184
0
                LogWarning("Block index entry %s not found for txid %s", candidate_block_hash.ToString(), tx_hash.ToString());
185
0
                continue;
186
0
            }
187
0
            if (!(block_index->nStatus & BLOCK_HAVE_DATA)) continue;
  Branch (187:17): [True: 0, False: 0]
188
0
            const FlatFilePos tx_position{block_index->nFile, block_index->nDataPos + key.pos.tx_offset_in_block};
189
0
            candidates.emplace_back(tx_position, candidate_block_hash, key.pos.block_seq, m_chainstate->m_chain.Contains(*block_index));
190
0
        }
191
0
    }
192
193
    // Prefer active-chain matches, then later-connected blocks.
194
0
    std::ranges::sort(candidates, std::greater{}, [](const Candidate& c) {
195
0
        return std::pair{c.in_active_chain, c.block_seq};
196
0
    });
197
198
0
    for (const auto& candidate : candidates) {
  Branch (198:32): [True: 0, False: 0]
199
0
        AutoFile file{m_chainstate->m_blockman.OpenBlockFile(candidate.tx_position, /*fReadOnly=*/true)};
200
0
        if (file.IsNull()) {
  Branch (200:13): [True: 0, False: 0]
201
0
            LogWarning("OpenBlockFile failed for txid %s", tx_hash.ToString());
202
0
            continue;
203
0
        }
204
0
        CTransactionRef tx;
205
0
        try {
206
0
            file >> TX_WITH_WITNESS(tx);
207
0
        } catch (const std::exception& e) {
208
0
            LogWarning("Deserialize or I/O error - %s", e.what());
209
0
            continue;
210
0
        }
211
0
        if (tx->GetHash() == tx_hash) {
  Branch (211:13): [True: 0, False: 0]
212
0
            return TxIndexResult{candidate.block_hash, std::move(tx)};
213
0
        }
214
0
    }
215
    // Fall back to legacy if no hashed entry matched. This makes misses pay an
216
    // extra lookup, but keeps existing full-txid entries readable after upgrade.
217
0
    return m_db->m_has_legacy ? FindLegacyTx(tx_hash) : std::nullopt;
  Branch (217:12): [True: 0, False: 0]
218
0
}
219
220
std::optional<TxIndexResult> TxIndex::FindLegacyTx(const Txid& tx_hash) const
221
0
{
222
0
    CDiskTxPos postx;
223
0
    if (!m_db->Read(txindex::LegacyTxKey(tx_hash), postx)) {
  Branch (223:9): [True: 0, False: 0]
224
0
        return std::nullopt;
225
0
    }
226
227
0
    AutoFile file{m_chainstate->m_blockman.OpenBlockFile(postx, /*fReadOnly=*/true)};
228
0
    if (file.IsNull()) {
  Branch (228:9): [True: 0, False: 0]
229
0
        LogError("OpenBlockFile failed");
230
0
        return std::nullopt;
231
0
    }
232
0
    CBlockHeader header;
233
0
    CTransactionRef tx;
234
0
    try {
235
0
        file >> header;
236
0
        file.seek(postx.nTxOffset, SEEK_CUR);
237
0
        file >> TX_WITH_WITNESS(tx);
238
0
    } catch (const std::exception& e) {
239
0
        LogError("Deserialize or I/O error - %s", e.what());
240
0
        return std::nullopt;
241
0
    }
242
0
    if (tx->GetHash() != tx_hash) {
  Branch (242:9): [True: 0, False: 0]
243
0
        LogError("txid mismatch");
244
0
        return std::nullopt;
245
0
    }
246
0
    return TxIndexResult{header.GetHash(), std::move(tx)};
247
0
}