Source code for btclib_node.chainstate

# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.

"""`Chainstate`: the block index, the UTXO set and the compact filter index.

All three share the one `db.KeyValueStore` `Chainstate` opens, told apart
by each's own key prefix -- `db.py`'s own docstring is where that shared
store's key order is argued. `block_index.BlockIndex` tracks headers and
which chain is active, `utxo_index.UtxoIndex` the spendable outputs on
it, and `filter_index.FilterIndex` the BIP157/BIP158 filters served over
p2p; `contextual.py` is the height- and time-dependent validation the
first of those calls before extending the active chain.

`flush` is what writes all three indexes' own staged changes in one
batch, and `close` calls it before closing the store -- `db.py`'s
docstring is where the crash this is the other half of is argued.
"""

from typing import TYPE_CHECKING

from btclib_node.db import KeyValueStore

from .block_index import BlockIndex
from .filter_index import FilterIndex
from .utxo_index import UtxoIndex

__all__ = ["Chainstate"]

if TYPE_CHECKING:
    from pathlib import Path

    from btclib_node.chains import Chain
    from btclib_node.log import Logger


[docs] class Chainstate: """The block index, the UTXO set and the compact filter index, together. The module docstring above is where the three, and the one `KeyValueStore` they share, are argued. """ def __init__(self, data_dir: Path, chain: Chain, logger: Logger) -> None: """Open the store under `data_dir` and build all three indexes on it.""" data_dir = data_dir / "chainstate" data_dir.mkdir(exist_ok=True, parents=True) self.db = KeyValueStore(data_dir) self.block_index = BlockIndex(self.db, chain, logger) self.utxo_index = UtxoIndex(self.db, logger) # `BlockIndex.init_from_db` walks the database until the first # key that is not a `blkinfo-`, and `cfilter-`/`cfheader-` sort # after those -- which is what makes the order these three are # built in not matter, rather than what makes this one right self.filter_index = FilterIndex(self.db, chain, logger) self.logger = logger
[docs] def flush(self) -> None: """Write every index's own staged changes, in one atomic batch. `main._finalize_fork` stages a connected or disconnected block's own status (`BlockIndex.stage_status`) and its filter (`FilterIndex.add_connected_block`) the same way `UtxoIndex` already staged its spends and creations, across more than one block; writing the three together here -- one `write_batch`, one commit -- is what keeps a status or a filter from ever landing on disk ahead of the UTXO set it was validated against. `db.py`'s own docstring argues why that has to hold. """ with self.db.write_batch() as wb: self.block_index.finalize(wb) self.utxo_index.finalize(wb) self.filter_index.finalize(wb)
[docs] def close(self) -> None: """Flush every staged index, then close the shared store. A clean close loses nothing staged -- the crash this store has to survive is one that never reaches this method at all, and `db.py`'s docstring is where what that crash costs is decided. Safe to call twice: `flush` needs the connection open, so a second call skips it and reaches only `KeyValueStore.close`'s own no-op on an already-closed store. """ self.logger.info("Closing Chainstate db") if not self.db.closed: self.flush() self.db.close()