-
Notifications
You must be signed in to change notification settings - Fork 37.7k
index: store per-block transaction locations for efficient lookups #32541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32541. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones:
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. |
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
Concept ACK Can you add the schema of the index and the expected arguments for the REST API to the pull request description? I was a bit confused at first if this now exposes the file position, but if I read it correctly now, this just allows querying a transaction by its index in the block. |
Thanks!
Sure - updated in #32541 (comment). |
Fixed a few issues (following SonarQube run). |
How does this compare to |
I have used fetching using the new index
fetching using
|
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
Rebased to fix #32541 (comment). |
As far as I understand the index makes this operation faster by not requiring to read the entire block and then iterating through the transactions to find the match, which I am guessing is what the last benchmark is showing. romanz, would this new endpoint be used while creating the entire index initially, or to serve certain requests? It is not quite clear to me when an indexing client wouldn't want to read through the entire block and instead only get its transactions selectively. |
Correct - the proposed index improves the performance of fetching a single transaction (similar to
I would like the new index to be used to serve history-related queries. You are right that during the history indexing process, the client doesn't need the proposed index, since it needs to read both the entire block (and undo) data in order to map from BTW, I am working on a proof-of-concept indexer (https://github.com/romanz/bindex-rs) which is using #32540 & #32541 - please let me know if there are any questions/comments/concerns :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm. I wonder if there should be a fallback?
Rebased over |
Ping :) |
024a91f
to
1b928f5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK 1b928f5
Cool way of compressing transaction metadata to significantly reduce on-disk footprint compared to TxIndex
and make it more likely this one fits in RAM. (Fine as long as external code can fetch by block hash + tx index instead of by txid).
Optimization as I understand it: All transactions within the same block have the same FlatFilePos
base, which is compressed by a factor of 128 thanks to TXS_PER_ROW
. Instead of storing an individual tx offset and size for each tx, it uses 2 offsets for a tx request, which allows re-use of offsets between adjacent txs.
Thanks for electrs!
Many thanks for the review! |
Applied a few more fixes: |
Ping :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 4441827
Sorry for the bag of nits, take from it what you will. The questions have higher priority for sure. Tried adding coverage of LocationsIndex
to test/functional/feature_init.py but ran into interference with TxIndex
, could be some more general issue though. Haven't had time to uncover the reason yet.
Suggestions+test branch:
4441827...hodlinator:bitcoin:pr/32541_suggestions
Further research idea:
It would be awesome to use sendfile(socket, filein, offset, count)
/TransmitFile()
for this to move that part to 100% kernel-space but not sure how we would cut through the abstractions for that:
https://www.man7.org/linux/man-pages/man2/sendfile.2.html
Edit regarding sendfile
:
Oof, this is somewhat impossible unless one turns off XOR obfuscation.
libevent had native support for sendfile
via evbuffer_add_file
and I did a quick & dirty implementation for /rest/block/0000000000000000000515e202c8ae73c8155fc472422d7593af87aa74f2cf3d.bin (fetching the taproot wizards 3.96MB block). Measured a 27% speedup (avg. 89.3ms vs 122.6ms for 1000 runs each).
src/index/locationsindex.cpp
Outdated
// file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
|
||
#include <blockencodings.h> | ||
#include <clientversion.h> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Unused: clientversion.h
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/index/locationsindex.cpp
Outdated
const uint32_t nTx = block.data->vtx.size(); | ||
uint32_t nTxOffset = HEADER_SIZE + GetSizeOfCompactSize(nTx); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: snake_case and narrower types:
const uint32_t nTx = block.data->vtx.size(); | |
uint32_t nTxOffset = HEADER_SIZE + GetSizeOfCompactSize(nTx); | |
Assume(block.data->vtx.size() <= std::numeric_limits<uint32_t>::max()); | |
const uint32_t tx_count{static_cast<uint32_t>(block.data->vtx.size())}; | |
uint32_t tx_offset{HEADER_SIZE + GetSizeOfCompactSize(tx_count)}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/index/locationsindex.cpp
Outdated
bool LocationsIndex::ReadRawTransaction(const uint256& block_hash, size_t i, std::vector<std::byte>& out) const | ||
{ | ||
uint32_t row = i / TXS_PER_ROW; // used to find the correct DB row | ||
const auto column = i % TXS_PER_ROW; // index within a single DB row |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Could make row
const
as well and nail down the types:
bool LocationsIndex::ReadRawTransaction(const uint256& block_hash, size_t i, std::vector<std::byte>& out) const | |
{ | |
uint32_t row = i / TXS_PER_ROW; // used to find the correct DB row | |
const auto column = i % TXS_PER_ROW; // index within a single DB row | |
bool LocationsIndex::ReadRawTransaction(const uint256& block_hash, uint32_t i, std::vector<std::byte>& out) const | |
{ | |
const uint32_t row{i / TXS_PER_ROW}; // used to find the correct DB row | |
const uint32_t column{i % TXS_PER_ROW}; // index within a single DB row |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/index/locationsindex.cpp
Outdated
return false; | ||
} | ||
|
||
out.resize(tx_size); // Zeroing of memory is intentional here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't see a good way around zeroing the memory, but comment makes it sound like a good thing?
I guess it's slightly better than returning a vector containing garbage upon failure, but even better would be to clear it upon failure?
nit: Could change:
- bool ReadRawTransaction(const uint256& block_hash, size_t i, std::vector<std::byte>& out) const;
+ std::vector<std::byte> ReadRawTransaction(const uint256& block_hash, uint32_t i) const;
And only return non-empty vector on success instead of having a separate bool? Somewhat unorthodox compared to other Index-implementations but more modern in that it exploits the fact that vectors are movable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/rest.cpp
Outdated
} else { | ||
strHex = HexStr(tx_bytes); | ||
} | ||
strHex.append("\n"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Might as well use the fact that it's only 1 char:
strHex.append("\n"); | |
strHex.push_back('\n'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/rest.cpp
Outdated
ChainstateManager* maybe_chainman = GetChainman(context, req); | ||
if (!maybe_chainman) return false; | ||
ChainstateManager& chainman = *maybe_chainman; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: chainman
is only used in the !locations_index
scenario, so could be skipped unless you want to always require one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/index/locationsindex.cpp
Outdated
{ | ||
fs::path path = gArgs.GetDataDirNet() / "indexes" / "locations"; | ||
fs::create_directories(path); | ||
|
||
m_db = std::make_unique<BaseIndex::DB>(path / "db", n_cache_size, f_memory, f_wipe); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure why explicit create_directories()
is needed, this seems to work:
{ | |
fs::path path = gArgs.GetDataDirNet() / "indexes" / "locations"; | |
fs::create_directories(path); | |
m_db = std::make_unique<BaseIndex::DB>(path / "db", n_cache_size, f_memory, f_wipe); | |
, m_db{std::make_unique<BaseIndex::DB>(gArgs.GetDataDirNet() / "indexes" / "locations" / "db", | |
n_cache_size, f_memory, f_wipe)} | |
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -53,7 +53,7 @@ def filter_output_indices_by_value(vouts, value): | |||
class RESTTest (BitcoinTestFramework): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Inline comment in random location to keep main thread cleaner).
Played around with adding coverage in feature_init.py
. However, I encountered some weird interaction between txindex and locationsindex. Not sure how incidental it is (going above a certain number of files) or whether some kind of interference is happening. See comment "If we add this, then we fail later while perturbing txindex":
diff --git a/test/functional/feature_init.py b/test/functional/feature_init.py
index 15b3e8595f..47e29655ce 100755
--- a/test/functional/feature_init.py
+++ b/test/functional/feature_init.py
@@ -49,7 +49,7 @@ class InitTest(BitcoinTestFramework):
def start_expecting_error(err_fragment):
node.assert_start_raises_init_error(
- extra_args=['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1', '-checkblocks=200', '-checklevel=4'],
+ extra_args=['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1', '-locationsindex=1', '-checkblocks=200', '-checklevel=4'],
expected_msg=err_fragment,
match=ErrorMatch.PARTIAL_REGEX,
)
@@ -77,6 +77,7 @@ class InitTest(BitcoinTestFramework):
b'txindex thread start',
b'block filter index thread start',
b'coinstatsindex thread start',
+ b'locationsindex thread start',
b'msghand thread start',
b'net thread start',
b'addcon thread start',
@@ -84,7 +85,7 @@ class InitTest(BitcoinTestFramework):
if self.is_wallet_compiled():
lines_to_terminate_after.append(b'Verifying wallet')
- args = ['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1']
+ args = ['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1', '-locationsindex=1']
for terminate_line in lines_to_terminate_after:
self.log.info(f"Starting node and will terminate after line {terminate_line}")
with node.busy_wait_for_debug_log([terminate_line]):
@@ -101,12 +102,13 @@ class InitTest(BitcoinTestFramework):
self.stop_node(0)
self.log.info("Test startup errors after removing certain essential files")
-
files_to_delete = {
'blocks/index/*.ldb': 'Error opening block database.',
'chainstate/*.ldb': 'Error opening coins database.',
'blocks/blk*.dat': 'Error loading block database.',
'indexes/txindex/MANIFEST*': 'LevelDB error: Corruption: CURRENT points to a non-existent file',
+ # If we add this, then we fail later while perturbing txindex!?:
+ #'indexes/locations/db/MANIFEST*': 'LevelDB error: Corruption: CURRENT points to a non-existent file',
# Removing these files does not result in a startup error:
# 'indexes/blockfilter/basic/*.dat', 'indexes/blockfilter/basic/db/*.*', 'indexes/coinstats/db/*.*',
# 'indexes/txindex/*.log', 'indexes/txindex/CURRENT', 'indexes/txindex/LOCK'
@@ -120,6 +122,8 @@ class InitTest(BitcoinTestFramework):
'indexes/coinstats/db/*.*': 'LevelDB error: Corruption',
'indexes/txindex/*.log': 'LevelDB error: Corruption',
'indexes/txindex/CURRENT': 'LevelDB error: Corruption',
+ 'indexes/locations/db/*.log': 'LevelDB error: Corruption',
+ 'indexes/locations/db/CURRENT': 'LevelDB error: Corruption',
# Perturbing these files does not result in a startup error:
# 'indexes/blockfilter/basic/*.dat', 'indexes/txindex/MANIFEST*', 'indexes/txindex/LOCK'
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, will investigate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
94389c2 seems to be passing (after a rebase over master
) 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hodlinator would it be OK to squash 94389c2 into bdd90ab?
src/node/blockstorage.h
Outdated
@@ -417,6 +417,8 @@ class BlockManager | |||
|
|||
bool ReadBlockUndo(CBlockUndo& blockundo, const CBlockIndex& index) const; | |||
|
|||
bool ReadTxFromBlock(CTransactionRef& tx, const FlatFilePos& block_pos, size_t tx_index) const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Could use more modern style instead of returning duplicate bool state (also nail down index type):
bool ReadTxFromBlock(CTransactionRef& tx, const FlatFilePos& block_pos, size_t tx_index) const; | |
CTransactionRef ReadTxFromBlock(const FlatFilePos& block_pos, uint32_t tx_index) const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
std::string param; | ||
const RESTResponseFormat rf = ParseDataFormat(param, uri_part); | ||
|
||
// request is sent over URI scheme /rest/txfromblock/blockhash-index |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you considered the possibility of supporting batches of block-hash+tx-index in one GET?
rest_getutxos()
does something similar:
Line 1007 in 4441827
//inputs is sent over URI scheme (/rest/getutxos/checkmempool/txid1-n/txid2-n/...) |
No sure if it's best practices in the REST world though. Another option could be using a JSON blob in a HTTP header, but that may be even more frowned upon.
cc @stickies-v who might have a feel for this kind of thing (came across his f959fc0).
4441827
to
5e1c80a
Compare
Many thanks for the review and the suggested fixes! |
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
Rebasing to resolve a conflict with |
37876b0
to
94389c2
Compare
Currently, electrs and other indexers map between an address/scripthash to the list of the relevant transactions.
However, in order to fetch those transactions from bitcoind, electrs relies on reading the whole block and post-filtering for a specific transaction1. Other indexers use a
txindex
to fetch a transaction using its txid 234.The above approach has significant storage and CPU overhead, since the
txid
is a pseudo-random 32-byte value.This PR is adding support for using the transaction's position within its block to be able to fetch it directly using REST API, using the following HTTP request (to fetch the
N
-th transaction fromBLOCKHASH
):If binary response format is used, the transaction data will be read directly from the storage and sent back to the client, without any deserialization overhead.
The resulting index is much smaller (allowing it to be cached in RAM):
The new index is using the following DB schema:
I am working on a proof-of-concept indexer (https://github.com/romanz/bindex-rs) which is using #32540 & #32541 - please let me know if there are any questions/comments/concerns :)
Footnotes
https://github.com/romanz/electrs/blob/master/doc/schema.md ↩
https://github.com/Blockstream/electrs/blob/new-index/doc/schema.md#txstore ↩
https://github.com/spesmilo/electrumx/blob/master/docs/HOWTO.rst#prerequisites ↩
https://github.com/cculianu/Fulcrum/blob/master/README.md#requirements ↩