txospenderindex.cpp raw
1 // Copyright (c) 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/txospenderindex.h>
6
7 #include <common/args.h>
8 #include <crypto/siphash.h>
9 #include <dbwrapper.h>
10 #include <flatfile.h>
11 #include <index/base.h>
12 #include <index/disktxpos.h>
13 #include <interfaces/chain.h>
14 #include <logging.h>
15 #include <node/blockstorage.h>
16 #include <primitives/block.h>
17 #include <primitives/transaction.h>
18 #include <random.h>
19 #include <serialize.h>
20 #include <streams.h>
21 #include <tinyformat.h>
22 #include <uint256.h>
23 #include <util/fs.h>
24 #include <validation.h>
25
26 #include <cstddef>
27 #include <cstdio>
28 #include <exception>
29 #include <ios>
30 #include <span>
31 #include <string>
32 #include <utility>
33 #include <vector>
34
35 /* The database is used to find the spending transaction of a given utxo.
36 * For every input of every transaction it stores a key that is a pair(siphash(input outpoint), transaction location on disk) and a zero-byte value.
37 * To find the spending transaction of an outpoint, we perform a range query on siphash(outpoint), and for each returned key load the transaction
38 * and return it if it does spend the provided outpoint.
39 */
40
41 // LevelDB key prefix. We only have one key for now but it will make it easier to add others if needed.
42 constexpr uint8_t DB_TXOSPENDERINDEX{'s'};
43
44 std::unique_ptr<TxoSpenderIndex> g_txospenderindex;
45
46 struct DBKey {
47 uint64_t hash;
48 CDiskTxPos pos;
49
50 explicit DBKey(const uint64_t& hash_in, const CDiskTxPos& pos_in) : hash(hash_in), pos(pos_in) {}
51
52 SERIALIZE_METHODS(DBKey, obj)
53 {
54 uint8_t prefix{DB_TXOSPENDERINDEX};
55 READWRITE(prefix);
56 if (prefix != DB_TXOSPENDERINDEX) {
57 throw std::ios_base::failure("Invalid format for spender index DB key");
58 }
59 READWRITE(obj.hash);
60 READWRITE(obj.pos);
61 }
62 };
63
64 TxoSpenderIndex::TxoSpenderIndex(std::unique_ptr<interfaces::Chain> chain, size_t n_cache_size, bool f_memory, bool f_wipe)
65 : BaseIndex(std::move(chain), "txospenderindex", "txospenderidx"), m_db{std::make_unique<DB>(gArgs.GetDataDirNet() / "indexes" / "txospenderindex" / "db", n_cache_size, f_memory, f_wipe, /*f_obfuscate=*/false, /*f_bloom=*/false)}
66 {
67 if (!m_db->Read("siphash_key", m_siphash_key)) {
68 FastRandomContext rng(false);
69 m_siphash_key = {rng.rand64(), rng.rand64()};
70 m_db->Write("siphash_key", m_siphash_key, /*fSync=*/ true);
71 }
72 }
73
74 interfaces::Chain::NotifyOptions TxoSpenderIndex::CustomOptions()
75 {
76 interfaces::Chain::NotifyOptions options;
77 options.disconnect_data = true;
78 return options;
79 }
80
81 static uint64_t CreateKeyPrefix(std::pair<uint64_t, uint64_t> siphash_key, const COutPoint& vout)
82 {
83 return PresaltedSipHasher(siphash_key.first, siphash_key.second)(vout.hash.ToUint256(), vout.n);
84 }
85
86 static DBKey CreateKey(std::pair<uint64_t, uint64_t> siphash_key, const COutPoint& vout, const CDiskTxPos& pos)
87 {
88 return DBKey(CreateKeyPrefix(siphash_key, vout), pos);
89 }
90
91 void TxoSpenderIndex::WriteSpenderInfos(const std::vector<std::pair<COutPoint, CDiskTxPos>>& items)
92 {
93 CDBBatch batch(*m_db);
94 for (const auto& [outpoint, pos] : items) {
95 DBKey key(CreateKey(m_siphash_key, outpoint, pos));
96 // The key encodes the spent outpoint hash and disk position. The value is only a marker.
97 // Older entries may contain serialized empty strings; FindSpender() reads only keys.
98 batch.Write(key, std::span<const std::byte>{});
99 }
100 m_db->WriteBatch(batch);
101 }
102
103
104 void TxoSpenderIndex::EraseSpenderInfos(const std::vector<std::pair<COutPoint, CDiskTxPos>>& items)
105 {
106 CDBBatch batch(*m_db);
107 for (const auto& [outpoint, pos] : items) {
108 batch.Erase(CreateKey(m_siphash_key, outpoint, pos));
109 }
110 m_db->WriteBatch(batch);
111 }
112
113 static std::vector<std::pair<COutPoint, CDiskTxPos>> BuildSpenderPositions(const interfaces::BlockInfo& block)
114 {
115 std::vector<std::pair<COutPoint, CDiskTxPos>> items;
116 items.reserve(block.data->vtx.size());
117
118 CDiskTxPos pos({block.file_number, block.data_pos}, GetSizeOfCompactSize(block.data->vtx.size()));
119 for (const auto& tx : block.data->vtx) {
120 if (!tx->IsCoinBase()) {
121 for (const auto& input : tx->vin) {
122 items.emplace_back(input.prevout, pos);
123 }
124 }
125 pos.nTxOffset += ::GetSerializeSize(TX_WITH_WITNESS(*tx));
126 }
127
128 return items;
129 }
130
131
132 bool TxoSpenderIndex::CustomAppend(const interfaces::BlockInfo& block)
133 {
134 WriteSpenderInfos(BuildSpenderPositions(block));
135 return true;
136 }
137
138 bool TxoSpenderIndex::CustomRemove(const interfaces::BlockInfo& block)
139 {
140 EraseSpenderInfos(BuildSpenderPositions(block));
141 return true;
142 }
143
144 util::Expected<TxoSpender, std::string> TxoSpenderIndex::ReadTransaction(const CDiskTxPos& tx_pos) const
145 {
146 AutoFile file{m_chainstate->m_blockman.OpenBlockFile(tx_pos, /*fReadOnly=*/true)};
147 if (file.IsNull()) {
148 return util::Unexpected("cannot open block");
149 }
150 CBlockHeader header;
151 TxoSpender spender;
152 try {
153 file >> header;
154 file.seek(tx_pos.nTxOffset, SEEK_CUR);
155 file >> TX_WITH_WITNESS(spender.tx);
156 spender.block_hash = header.GetHash();
157 return spender;
158 } catch (const std::exception& e) {
159 return util::Unexpected(e.what());
160 }
161 }
162
163 util::Expected<std::optional<TxoSpender>, std::string> TxoSpenderIndex::FindSpender(const COutPoint& txo) const
164 {
165 const uint64_t prefix{CreateKeyPrefix(m_siphash_key, txo)};
166 std::unique_ptr<CDBIterator> it(m_db->NewIterator());
167 DBKey key(prefix, CDiskTxPos());
168
169 // find all keys that start with the outpoint hash, load the transaction at the location specified in the key
170 // and return it if it does spend the provided outpoint
171 for (it->Seek(std::pair{DB_TXOSPENDERINDEX, prefix}); it->Valid() && it->GetKey(key) && key.hash == prefix; it->Next()) {
172 if (const auto spender{ReadTransaction(key.pos)}) {
173 for (const auto& input : spender->tx->vin) {
174 if (input.prevout == txo) {
175 return std::optional{*spender};
176 }
177 }
178 } else {
179 LogError("Deserialize or I/O error - %s", spender.error());
180 return util::Unexpected{strprintf("IO error finding spending tx for outpoint %s:%d.", txo.hash.GetHex(), txo.n)};
181 }
182 }
183 return util::Expected<std::optional<TxoSpender>, std::string>(std::nullopt);
184 }
185
186 BaseIndex::DB& TxoSpenderIndex::GetDB() const { return *m_db; }
187