Coverage Report

Created: 2026-09-14 20:36

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/tmp/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
46
{
46
46
    std::pair<uint64_t, uint64_t> salt;
47
46
    if (!db.Read(txindex::DB_TXID_HASH_SALT, salt)) {
48
22
        FastRandomContext rng{};
49
22
        salt = {rng.rand64(), rng.rand64()};
50
22
        db.Write(txindex::DB_TXID_HASH_SALT, salt, /*fSync=*/true);
51
22
    }
52
46
    return SipHasher13UJ{salt.first, salt.second};
53
46
}
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
92
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
49
    DB(n_cache_size, f_memory, f_wipe,
87
49
       /*has_legacy=*/!f_memory && !f_wipe && CDBWrapper::HasKeyStartingWith(TxIndexDBPath(), txindex::DB_TXINDEX))
88
49
{}
89
90
TxIndex::DB::DB(size_t n_cache_size, bool f_memory, bool f_wipe, bool has_legacy) :
91
46
    BaseIndex::DB(TxIndexDBPath(), n_cache_size, f_memory, f_wipe, /*f_obfuscate=*/false, /*f_bloom=*/has_legacy),
92
46
    m_hasher{ReadOrCreateTxidHasher(*this)},
93
46
    m_has_legacy{has_legacy}
94
46
{}
95
96
CBlockLocator TxIndex::DB::ReadBestBlock() const
97
52
{
98
52
    CBlockLocator locator;
99
52
    if (Read(txindex::DB_BEST_BLOCK_V2, locator)) {
100
25
        return locator;
101
25
    }
102
    // If we don't have a locator yet, start from the legacy best block.
103
27
    return BaseIndex::DB::ReadBestBlock();
104
52
}
105
106
void TxIndex::DB::WriteBestBlock(CDBBatch& batch, const CBlockLocator& locator)
107
55
{
108
55
    batch.Write(txindex::DB_BEST_BLOCK_V2, locator);
109
55
}
110
111
void TxIndex::DB::WriteTxs(const interfaces::BlockInfo& block)
112
4.64k
{
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
4.64k
    if (Exists(txindex::BlockHashKey{block.hash})) return;
117
118
4.14k
    uint32_t block_seq{0};
119
4.14k
    Read(txindex::DB_NEXT_BLOCK_SEQ, block_seq);
120
121
4.14k
    CDBBatch batch(*this);
122
4.14k
    batch.Write(txindex::BlockHashKey{block.hash}, block_seq);
123
4.14k
    batch.Write(txindex::BlockSeqKey{block_seq}, block.hash);
124
4.14k
    batch.Write(txindex::DB_NEXT_BLOCK_SEQ, block_seq + 1);
125
4.14k
    uint32_t tx_offset_in_block{txindex::BLOCK_HEADER_SIZE + GetSizeOfCompactSize(block.data->vtx.size())};
126
4.29k
    for (const auto& tx : block.data->vtx) {
127
4.29k
        const txindex::DBKey key{txindex::CreateKeyPrefix(m_hasher, tx->GetHash()),
128
4.29k
                                 txindex::BlockTxPosition{block_seq, tx_offset_in_block}};
129
4.29k
        batch.Write(key, txindex::EMPTY_VALUE);
130
4.29k
        tx_offset_in_block += tx->ComputeTotalSize();
131
4.29k
    }
132
4.14k
    WriteBatch(batch);
133
4.14k
}
134
135
TxIndex::TxIndex(std::unique_ptr<interfaces::Chain> chain, size_t n_cache_size, bool f_memory, bool f_wipe)
136
49
    : BaseIndex(std::move(chain), "txindex", "txidx"), m_db(std::make_unique<TxIndex::DB>(n_cache_size, f_memory, f_wipe))
137
49
{
138
49
    if (m_db->m_has_legacy) {
139
2
        LogInfo("txindex contains entries in the legacy format, which uses excessive disk space. "
140
2
                "To reclaim disk space, stop the node, delete %s and restart to rebuild the index.",
141
2
                fs::PathToString(TxIndexDBPath()));
142
2
    }
143
49
}
144
145
46
TxIndex::~TxIndex() = default;
146
147
bool TxIndex::CustomAppend(const interfaces::BlockInfo& block)
148
4.66k
{
149
    // Exclude genesis block transaction because outputs are not spendable.
150
4.66k
    if (block.height == 0) return true;
151
152
4.66k
    assert(block.data);
153
4.64k
    m_db->WriteTxs(block);
154
4.64k
    return true;
155
4.64k
}
156
157
219
BaseIndex::DB& TxIndex::GetDB() const { return *m_db; }
158
159
std::optional<TxIndexResult> TxIndex::FindTx(const Txid& tx_hash) const
160
260
{
161
260
    struct Candidate {
162
260
        FlatFilePos tx_position;
163
260
        uint256 block_hash;
164
260
        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
260
        bool in_active_chain;
169
260
    };
170
260
    std::vector<Candidate> candidates;
171
260
    {
172
260
        std::unique_ptr<CDBIterator> it{m_db->NewIterator()};
173
260
        const txindex::TxHashKeyPrefix prefix{txindex::CreateKeyPrefix(m_db->m_hasher, tx_hash)};
174
260
        txindex::DBKey key{prefix, {}};
175
416
        for (it->Seek(key); it->Valid() && it->GetKey(key) && key.hash_prefix == prefix; it->Next()) {
176
156
            uint256 candidate_block_hash;
177
156
            if (!m_db->Read(txindex::BlockSeqKey{key.pos.block_seq}, candidate_block_hash)) {
178
0
                LogWarning("Block sequence %u not found for txid %s", key.pos.block_seq, tx_hash.ToString());
179
0
                continue;
180
0
            }
181
156
            LOCK(cs_main);
182
156
            const CBlockIndex* block_index{m_chainstate->m_blockman.LookupBlockIndex(candidate_block_hash)};
183
156
            if (!block_index) {
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
156
            if (!(block_index->nStatus & BLOCK_HAVE_DATA)) continue;
188
156
            const FlatFilePos tx_position{block_index->nFile, block_index->nDataPos + key.pos.tx_offset_in_block};
189
156
            candidates.emplace_back(tx_position, candidate_block_hash, key.pos.block_seq, m_chainstate->m_chain.Contains(*block_index));
190
156
        }
191
260
    }
192
193
    // Prefer active-chain matches, then later-connected blocks.
194
260
    std::ranges::sort(candidates, std::greater{}, [](const Candidate& c) {
195
8
        return std::pair{c.in_active_chain, c.block_seq};
196
8
    });
197
198
260
    for (const auto& candidate : candidates) {
199
154
        AutoFile file{m_chainstate->m_blockman.OpenBlockFile(candidate.tx_position, /*fReadOnly=*/true)};
200
154
        if (file.IsNull()) {
201
0
            LogWarning("OpenBlockFile failed for txid %s", tx_hash.ToString());
202
0
            continue;
203
0
        }
204
154
        CTransactionRef tx;
205
154
        try {
206
154
            file >> TX_WITH_WITNESS(tx);
207
154
        } catch (const std::exception& e) {
208
0
            LogWarning("Deserialize or I/O error - %s", e.what());
209
0
            continue;
210
0
        }
211
154
        if (tx->GetHash() == tx_hash) {
212
153
            return TxIndexResult{candidate.block_hash, std::move(tx)};
213
153
        }
214
154
    }
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
107
    return m_db->m_has_legacy ? FindLegacyTx(tx_hash) : std::nullopt;
218
260
}
219
220
std::optional<TxIndexResult> TxIndex::FindLegacyTx(const Txid& tx_hash) const
221
3
{
222
3
    CDiskTxPos postx;
223
3
    if (!m_db->Read(txindex::LegacyTxKey(tx_hash), postx)) {
224
0
        return std::nullopt;
225
0
    }
226
227
3
    AutoFile file{m_chainstate->m_blockman.OpenBlockFile(postx, /*fReadOnly=*/true)};
228
3
    if (file.IsNull()) {
229
0
        LogError("OpenBlockFile failed");
230
0
        return std::nullopt;
231
0
    }
232
3
    CBlockHeader header;
233
3
    CTransactionRef tx;
234
3
    try {
235
3
        file >> header;
236
3
        file.seek(postx.nTxOffset, SEEK_CUR);
237
3
        file >> TX_WITH_WITNESS(tx);
238
3
    } catch (const std::exception& e) {
239
0
        LogError("Deserialize or I/O error - %s", e.what());
240
0
        return std::nullopt;
241
0
    }
242
3
    if (tx->GetHash() != tx_hash) {
243
0
        LogError("txid mismatch");
244
0
        return std::nullopt;
245
0
    }
246
3
    return TxIndexResult{header.GetHash(), std::move(tx)};
247
3
}