Undo stale block entries during reorgs & related optimizations#174
Undo stale block entries during reorgs & related optimizations#174RCasatta merged 15 commits intoBlockstream:new-indexfrom
Conversation
It makes more sense there, since it doesn't depend on any of the data added to the txstore in the first `add` stage. And it needs to be there, for the followup commit that assumes all entries in the history db can be safely deleted when undoing blocks.
992d996 to
ae23bc5
Compare
|
I still need to review the code... After reading the description I wonder if you considered to move to a single db with multiple column families since we are migrating |
Prior to this change, history index entries created by stale blocks would remain in the history DB and only get discarded at read time. This change explicitly removes history entries when a reorg occurs, so we can assume all indexed entries correspond to blocks currently still part of the best chain. This enables optimizing some db lookups (in the followup commits), since readers no longer need to account for stale entries. (Note schema.md was only corrected to match the existing schema, 'D' rows were already being kept for both the history and txstore dbs.)
Iterating history db entries now involves a single sequential db scan (plus reads into the in-memory HeaderList), without the per-tx random access db reads that were previously needed to verify confirmation status.
Changed from an index of `txid -> Set<blockhash>` to `txid -> blockheight` - Instead of a list of blocks seen to include the txid (including stale blocks), map the txid directly to the single block that confirmed it and is still part of the best chain. - Identify blocks by their height instead of their hash. Previously it was necessary to keep the hash to ensure it is still part of the best chain, but now we can assume that it is. - Move the index from the txstore db to the history db, so that its entries will get undone during reorgs.
Changed from an index of `funding_txid:vout -> Set<spending_txid:vin>` to `funding_txid:vout -> spending_txid:vin||spending_height` - Instead of a list of inputs seen to spend the outpoint, map the outpoint directly to the single spending input that is still part of the best chain. - Keep the height of the spending transaction, too. This reduces the number of db reads per spend lookup from 2 to 1.
Now possible with the V2 schema, since the exact TxEdge row key can be derived from the funding_txid:vout alone (previously the key also included the spending_txid, requiring a prefix scan for each lookup).
Now possible with the V2 schema, since the exact TxConf row key can be derived from the txid alone (previously the key also included the block, requiring a prefix scan for each lookup). This isn't used anywhere yet, but will be used in a followup commit for the DB migration script (and could potentially be used for a new public API endpoint). Exposed as a standalone function so that it can be used directly with a `DB`, without having to construct the full `ChainQuery` with a `Daemon`.
We definitely should move to using column families, but its a rather invasive change and I'm not sure it should be bundled with this PR. It would also be a much larger/slower migration, basically reading out the entire DB and writing it back in. I'm not sure if it crosses that threshold, but I guess that at some point for large/complex migrations it might make more sense to just bite the bullet and do a full re-index instead? |
ae23bc5 to
416cb4c
Compare
|
I pushed a couple bug fixes, dafcd6d (stale entries in The latter bug already exists in the main |
- Change lookup_txns to use MultiGet - Use lookup_txns for block transactions and reconstruction too (GET /block/:hash/txs and GET /block/:hash/raw) (This was already possible with the V1 schema, but related to and builds upon the other V2 changes.) Plus some related changes: - Remove expensive sanity check assertion in lookup_txn (involved txid computation and wasn't really necessary) - Add test for raw block reconstruction
Previously each key/value read during iteration was getting duplicated 😮 (This doesn't strictly belong to the PR its included in, but it will greatly benefit the DB migration script.)
Also adds HeaderList::best_height() to help avoid off-by-one errors for the chain length vs tip height (like I initially made when implementing this >.<), and to make getting the tip height of an empty HeaderList an explicit error (previously it over overflow and return usize::MAX).
Prior to this fix, `Indexer::update()` would panic on the `assert_eq!(tip, *headers.tip())` assertion when handling reorgs that shorten the existing chain without adding any blocks to replace them. This should not normally happen, but might due to manual `invalidateblock`. For example, this will reproduce the panic: `bitcoin-cli invalidateblock $(bitcoin-cli getbestblockhash)`
22cf7f9 to
ae45516
Compare
|
I slightly squashed some commits, the only change from your ACK'd 22cf7f9 is the fix for "already a
Cool :-) |
This PR implements reorg handling that actively removes DB entries originating from stale blocks as the reorg occurs instead of discarding them at read time, plus several optimizations made possible by it. Some notes:
The original motivation for this PR - optimizing
TxHistorylookups - already made a DB reindex/migration necessary (to cleanup stale entries), so I ended up bundling in a couple further related optimizations that required DB schema changes, for theTxEdgeandTxConfindexes.This implementation makes the distinction that entries created by stale blocks in the
historyDB should get undone during reorgs, whiletxstoreDB entries are kept.This is partially because
txstoreentries from stale blocks could be useful for archival purposes (they're not all fully available through the APIs, but could be made to be) and are cheap to keep around (not many and not in the way of history index lookups), but more importantly also because non-unique keys cannot be 'undone' by deletion when handling reorgs (and keeping them in thetxstoreseemed easier than making an exception for them).Because of this (and because a migration was already needed..), the address prefix search index (
A) was moved to thetxstoreDB, while the confirmations index (C) was moved to thehistoryDB.Care was taken to ensure consistency of the public APIs - it is never possible for blocks to be visible (e.g. in
GET /blocks/tiporGET /block/:hash) without the corresponding history entries from those blocks being indexed and visible too (e.g. inGET /address/:address/txsorGET /tx/:txid/:vout/outspend), or vice-versa.The consistency is guaranteed by ensuring that in-progress DB writes always refer to heights that don't yet exists or were removed from the
HeaderList, which keeps partial in-progress writes publicly 'invisible' until processing is completed. This also means that the visible chain tip will temporarily drop down to the common ancestor while the reorg is being processed, which was not the case previously.(Preserving both a monotonic chain tip and full consistency under the new design is possible but would require holding an exclusive lock on the
HeaderListfor the entire reorg processing duration, which seems undesirable.)The DB schema version was bumped from
1to2, with a migration script available as adb-migrate-v1-to-v2binary. Example use:cargo run --bin db-migrate-v1-to-v2 -- -vvv --network mainnet --db-dir dbThe migration could benefit from setting a larger
--db-write-buffer-size-mbfor rocksdb (configurable as an electrs CLI option).Migration of Elements DBs is supported too, using
--features liquid.