packages feed

haskoin-node-2.4.0: src/Haskoin/Node/Chain/Database.hs

-- |
-- Module      : Haskoin.Node.Chain.Database
-- Copyright   : (c) Haskoin Developers
-- License     : MIT
-- Maintainer  : jprupp@protonmail.ch
-- Stability   : experimental
-- Portability : POSIX
--
-- Storage of block headers in RocksDB, for "Haskoin.Node.Chain".
--
-- Headers are stored as 'BlockNode' values in the configured column family, or
-- the default one, under keys starting with byte @0x90@. The best block is
-- stored under key @0x91@ and a data version under key @0x92@. If the data
-- version does not match, keys @0x90@ and @0x91@ are deleted when the chain
-- starts. Applications that share the database must not use these prefixes.
--
-- = Why it works this way
--
-- * __Headers are stored, not kept in memory.__ Storing them lets a restart
--   resume where it stopped instead of downloading the whole chain again.
--
-- * __Through haskoin-core's interface.__ The header functions of
--   haskoin-core, such as 'connectBlocks' and 'getAncestor', run in any monad
--   with a 'BlockHeaders' instance. 'runHeaders' runs them against the
--   database, so validation and storage are not reimplemented here. The monad
--   that carries the database is a 'ReaderT', and it stays inside this
--   module: everything else calls 'runHeaders' and works in plain 'IO'.
--
-- * __Separate from syncing.__ This module only knows how headers are laid
--   out on disk. The chain process decides what to store and when.
module Haskoin.Node.Chain.Database
  ( ChainDB (..),
    initChainDB,
    runHeaders,
  )
where

import Control.Monad (guard, unless, when)
import Control.Monad.Trans.Reader (ReaderT (..))
import Data.ByteString qualified as B
import Data.Maybe (isNothing)
import Data.Serialize (Serialize (..), getWord8, putWord8)
import Data.Word (Word32)
import Database.RocksDB (ColumnFamily, DB)
import Database.RocksDB qualified as R
import Database.RocksDB.Query
  ( Key,
    KeyValue,
    insert,
    insertCF,
    insertOp,
    insertOpCF,
    retrieveCommon,
    writeBatch,
  )
import Haskoin hiding (Key)

-- | Where headers are stored: a database and an optional column family.
data ChainDB = ChainDB
  { -- | database handle
    db :: !DB,
    -- | column family, or the default one if 'Nothing'
    cf :: !(Maybe ColumnFamily)
  }

-- | Key of the data version.
data VersionKey = VersionKey
  deriving stock (Eq, Show)

instance Serialize VersionKey where
  get = getWord8 >>= guard . (== 0x92) >> return VersionKey
  put VersionKey = putWord8 0x92

instance Key VersionKey

instance KeyValue VersionKey Word32

-- | Key of a block header.
newtype BlockHeaderKey = BlockHeaderKey BlockHash
  deriving stock (Eq, Show)

instance Serialize BlockHeaderKey where
  get = getWord8 >>= guard . (== 0x90) >> BlockHeaderKey <$> get
  put (BlockHeaderKey bh) = putWord8 0x90 >> put bh

instance KeyValue BlockHeaderKey BlockNode

-- | Key of the best block.
data BestBlockKey = BestBlockKey
  deriving stock (Eq, Show)

instance Serialize BestBlockKey where
  get = getWord8 >>= guard . (== 0x91) >> return BestBlockKey
  put BestBlockKey = putWord8 0x91

instance KeyValue BestBlockKey BlockNode

-- | Run header functions of haskoin-core against the database.
runHeaders ::
  ChainDB -> (forall n. (BlockHeaders n) => n a) -> IO a
runHeaders c action = (runReaderT action c)

-- | Read and write headers in the database.
instance BlockHeaders (ReaderT ChainDB IO) where
  addBlockHeader bn = ReaderT $ \c -> insertCommon c (nodeKey bn) bn
  getBlockHeader bh =
    ReaderT $ \c -> retrieveCommon c.db c.cf (BlockHeaderKey bh)
  getBestBlockHeader = ReaderT $ \c ->
    (retrieveCommon c.db c.cf BestBlockKey) >>= \case
      Nothing -> error "Could not get best block from database"
      Just b -> return b
  setBestBlockHeader bn = ReaderT $ \c -> insertCommon c BestBlockKey bn
  addBlockHeaders bns =
    ReaderT $ \c -> writeBatch c.db $ map (op c) bns
    where
      op c bn = case c.cf of
        Nothing -> insertOp (nodeKey bn) bn
        Just cf -> insertOpCF cf (nodeKey bn) bn

nodeKey :: BlockNode -> BlockHeaderKey
nodeKey bn = BlockHeaderKey (headerHash bn.header)

insertCommon ::
  (KeyValue k v, Serialize k, Serialize v) =>
  ChainDB ->
  k ->
  v ->
  IO ()
insertCommon c k v = case c.cf of
  Nothing -> insert c.db k v
  Just cf -> insertCF c.db cf k v

-- | Version of the data layout.
dataVersion :: Word32
dataVersion = 1

-- | Prepare the database for a network. If its data version is not the
-- current one, headers and best block are deleted first. An empty database
-- gets the genesis block of the network as its best block.
initChainDB :: Network -> ChainDB -> IO ()
initChainDB net c = do
  ver <- retrieveCommon c.db c.cf VersionKey
  unless (ver == Just dataVersion) $ purgeChainDB c
  insertCommon c VersionKey dataVersion
  best <- retrieveCommon c.db c.cf BestBlockKey
  when (isNothing (best :: Maybe BlockNode)) $ runHeaders c $ do
    addBlockHeader (genesisNode net)
    setBestBlockHeader (genesisNode net)

-- | Delete headers and best block from the column family that holds the
-- chain. Keys are deleted one at a time as the iterator walks them, so that
-- memory use does not grow with the size of the database.
purgeChainDB :: ChainDB -> IO ()
purgeChainDB c = withIter $ \it -> do
  R.iterSeek it (B.singleton 0x90)
  let go =
        R.iterKey it >>= \case
          Just k | B.take 1 k `elem` [B.singleton 0x90, B.singleton 0x91] -> do
            case c.cf of
              Nothing -> R.delete c.db k
              Just cf -> R.deleteCF c.db cf k
            R.iterNext it
            go
          _ -> return ()
  go
  where
    withIter = case c.cf of
      Nothing -> R.withIter c.db
      Just cf -> R.withIterCF c.db cf