haskoin-node-1.0.0: src/Haskoin/Node/Chain.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Haskoin.Node.Chain
( ChainConfig (..),
ChainEvent (..),
Chain,
withChain,
chainGetBlock,
chainGetBest,
chainGetAncestor,
chainGetParents,
chainGetSplitBlock,
chainPeerConnected,
chainPeerDisconnected,
chainIsSynced,
chainBlockMain,
chainHeaders,
)
where
import Control.Monad (forM_, forever, guard, when)
import Control.Monad.Except (runExceptT, throwError)
import Control.Monad.Logger
( MonadLoggerIO,
logDebugS,
logErrorS,
logInfoS,
)
import Control.Monad.Reader
( MonadReader,
ReaderT (..),
asks,
runReaderT,
)
import Control.Monad.Trans (lift)
import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)
import qualified Data.ByteString as B
import Data.Function (on)
import Data.List (delete, nub)
import Data.Maybe (isJust, isNothing)
import Data.Serialize
( Serialize,
get,
getWord8,
put,
putWord8,
)
import Data.String.Conversions (cs)
import Data.Time.Clock
( NominalDiffTime,
UTCTime,
diffUTCTime,
getCurrentTime,
)
import Data.Time.Clock.POSIX
( posixSecondsToUTCTime,
utcTimeToPOSIXSeconds,
)
import Data.Word (Word32)
import Database.RocksDB (ColumnFamily, DB)
import qualified Database.RocksDB as R
import Database.RocksDB.Query
( Key,
KeyValue,
insert,
insertCF,
insertOp,
insertOpCF,
retrieveCommon,
writeBatch,
)
import Haskoin
( BlockHash,
BlockHeader (..),
BlockHeaders (..),
BlockHeight,
BlockNode (..),
GetHeaders (..),
Message (..),
Network,
blockLocator,
connectBlocks,
genesisNode,
getAncestor,
headerHash,
splitPoint,
)
import Haskoin.Node.Peer
import Haskoin.Node.PeerMgr (myVersion)
import NQE
( Mailbox,
Publisher,
newMailbox,
publish,
receive,
send,
)
import System.Random (randomRIO)
import UnliftIO
( MonadIO,
MonadUnliftIO,
TVar,
atomically,
liftIO,
link,
modifyTVar,
newTVarIO,
readTVar,
readTVarIO,
withAsync,
writeTVar,
)
import UnliftIO.Concurrent (threadDelay)
-- | Mailbox for chain header syncing process.
data Chain = Chain
{ mailbox :: !(Mailbox ChainMessage),
reader :: !ChainReader
}
instance Eq Chain where
(==) = (==) `on` (.mailbox)
-- | Configuration for chain syncing process.
data ChainConfig = ChainConfig
{ -- | database handle
db :: !DB,
-- | column family
cf :: !(Maybe ColumnFamily),
-- | network constants
net :: !Network,
-- | send header chain events here
pub :: !(Publisher ChainEvent),
-- | timeout in seconds
timeout :: !NominalDiffTime
}
data ChainMessage
= ChainHeaders !Peer ![BlockHeader]
| ChainPeerConnected !Peer
| ChainPeerDisconnected !Peer
| ChainPing
-- | Events originating from chain syncing process.
data ChainEvent
= -- | chain has new best block
ChainBestBlock !BlockNode
| -- | chain is in sync with the network
ChainSynced !BlockNode
deriving (Eq, Show)
type MonadChain m =
( MonadLoggerIO m,
MonadUnliftIO m,
MonadReader ChainReader m
)
-- | State and configuration.
data ChainReader = ChainReader
{ -- | placeholder for upstream data
config :: !ChainConfig,
-- | mutable state for header synchronization
state :: !(TVar ChainState)
}
-- | Database key for version.
data ChainDataVersionKey = ChainDataVersionKey
deriving (Eq, Ord, Show)
instance Key ChainDataVersionKey
instance KeyValue ChainDataVersionKey Word32
instance Serialize ChainDataVersionKey where
get = do
guard . (== 0x92) =<< getWord8
return ChainDataVersionKey
put ChainDataVersionKey = putWord8 0x92
data ChainSync = ChainSync
{ peer :: !Peer,
timestamp :: !UTCTime,
best :: !(Maybe BlockNode)
}
-- | Mutable state for the header chain process.
data ChainState = ChainState
{ -- | peer to sync against and time of last received message
syncing :: !(Maybe ChainSync),
-- | queue of peers to sync against
peers :: ![Peer],
-- | has the header chain ever been considered synced?
beenInSync :: !Bool
}
-- | Key for block header in database.
newtype BlockHeaderKey = BlockHeaderKey BlockHash deriving (Eq, Show)
instance Serialize BlockHeaderKey where
get = do
guard . (== 0x90) =<< getWord8
BlockHeaderKey <$> get
put (BlockHeaderKey bh) = do
putWord8 0x90
put bh
-- | Key for best block in database.
data BestBlockKey = BestBlockKey deriving (Eq, Show)
instance KeyValue BlockHeaderKey BlockNode
instance KeyValue BestBlockKey BlockNode
instance Serialize BestBlockKey where
get = do
guard . (== 0x91) =<< getWord8
return BestBlockKey
put BestBlockKey = putWord8 0x91
instance (MonadIO m) => BlockHeaders (ReaderT ChainConfig m) where
addBlockHeader bn = do
db <- asks (.db)
asks (.cf) >>= \case
Nothing -> insert db (BlockHeaderKey h) bn
Just cf -> insertCF db cf (BlockHeaderKey h) bn
where
h = headerHash bn.header
getBlockHeader bh = do
db <- asks (.db)
mcf <- asks (.cf)
retrieveCommon db mcf (BlockHeaderKey bh)
getBestBlockHeader = do
db <- asks (.db)
mcf <- asks (.cf)
retrieveCommon db mcf BestBlockKey >>= \case
Nothing -> error "Could not get best block from database"
Just b -> return b
setBestBlockHeader bn = do
db <- asks (.db)
asks (.cf) >>= \case
Nothing -> insert db BestBlockKey bn
Just cf -> insertCF db cf BestBlockKey bn
addBlockHeaders bns = do
db <- asks (.db)
mcf <- asks (.cf)
writeBatch db (map (f mcf) bns)
where
h bn = headerHash bn.header
f Nothing bn = insertOp (BlockHeaderKey (h bn)) bn
f (Just cf) bn = insertOpCF cf (BlockHeaderKey (h bn)) bn
instance (MonadIO m) => BlockHeaders (ReaderT Chain m) where
getBlockHeader bh = ReaderT $ chainGetBlock bh
getBestBlockHeader = ReaderT chainGetBest
addBlockHeader _ = undefined
setBestBlockHeader _ = undefined
addBlockHeaders _ = undefined
withBlockHeaders :: (MonadChain m) => ReaderT ChainConfig m a -> m a
withBlockHeaders f = do
cfg <- asks (.config)
runReaderT f cfg
withChain ::
(MonadUnliftIO m, MonadLoggerIO m) =>
ChainConfig ->
(Chain -> m a) ->
m a
withChain cfg action = do
(inbox, mailbox) <- newMailbox
$(logDebugS) "Chain" "Starting chain actor"
st <-
newTVarIO
ChainState
{ syncing = Nothing,
beenInSync = False,
peers = []
}
let rd = ChainReader {config = cfg, state = st}
ch = Chain {reader = rd, mailbox = mailbox}
runReaderT initChainDB rd
withAsync (main_loop ch rd inbox) $ \a ->
link a >> action ch
where
main_loop ch rd inbox =
withSyncLoop ch $
runReaderT (run inbox) rd
run inbox = do
withBlockHeaders getBestBlockHeader
>>= chainEvent . ChainBestBlock
forever $ do
$(logDebugS) "Chain" "Awaiting event..."
msg <- receive inbox
chainMessage msg
chainEvent :: (MonadChain m) => ChainEvent -> m ()
chainEvent e = do
pub <- asks (.config.pub)
case e of
ChainBestBlock b ->
$(logInfoS) "Chain" $
"Best block header at height: "
<> cs (show b.height)
ChainSynced b ->
$(logInfoS) "Chain" $
"Headers in sync at height: "
<> cs (show b.height)
publish e pub
processHeaders :: (MonadChain m) => Peer -> [BlockHeader] -> m ()
processHeaders p hs = do
$(logDebugS) "Chain" $
"Processing "
<> cs (show (length hs))
<> " headers from peer: "
<> p.label
net <- asks (.config.net)
now <- liftIO getCurrentTime
pbest <- withBlockHeaders getBestBlockHeader
importHeaders net now hs >>= \case
Left e -> do
$(logErrorS) "Chain" $
"Could not connect headers from peer: "
<> p.label
e `killPeer` p
Right done -> do
setLastReceived
best <- withBlockHeaders getBestBlockHeader
when (pbest.header /= best.header) $
chainEvent (ChainBestBlock best)
if done
then do
MSendHeaders `sendMessage` p
finishPeer p
syncNewPeer
syncNotif
else syncPeer p
syncNewPeer :: (MonadChain m) => m ()
syncNewPeer =
getSyncingPeer >>= \case
Just _ -> return ()
Nothing ->
nextPeer >>= \case
Nothing -> return ()
Just p -> do
$(logDebugS) "Chain" $
"Syncing against peer: " <> p.label
syncPeer p
syncNotif :: (MonadChain m) => m ()
syncNotif =
notifySynced >>= \case
False -> return ()
True ->
withBlockHeaders getBestBlockHeader
>>= chainEvent . ChainSynced
syncPeer :: (MonadChain m) => Peer -> m ()
syncPeer p = do
t <- liftIO getCurrentTime
m <-
chainSyncingPeer >>= \case
Just
ChainSync
{ peer = s,
best = m
}
| p == s -> syncing_me t m
| otherwise -> return Nothing
Nothing -> syncing_new t
forM_ m $ \g -> do
$(logDebugS) "Chain" $
"Requesting headers from peer: "
<> p.label
MGetHeaders g `sendMessage` p
where
syncing_new t =
setSyncingPeer p >>= \case
False -> return Nothing
True -> do
$(logDebugS) "Chain" $
"Locked peer: " <> p.label
h <- withBlockHeaders getBestBlockHeader
Just <$> syncHeaders t h p
syncing_me t m = do
h <- case m of
Nothing -> withBlockHeaders getBestBlockHeader
Just h -> return h
Just <$> syncHeaders t h p
chainMessage :: (MonadChain m) => ChainMessage -> m ()
chainMessage (ChainHeaders p hs) =
processHeaders p hs
chainMessage (ChainPeerConnected p) = do
$(logDebugS) "Chain" $ "Peer connected: " <> p.label
addPeer p
syncNewPeer
chainMessage (ChainPeerDisconnected p) = do
$(logDebugS) "Chain" $ "Peer disconnected: " <> p.label
finishPeer p
syncNewPeer
chainMessage ChainPing = do
$(logDebugS) "Chain" "Internal clock event"
to <- asks (.config.timeout)
now <- liftIO getCurrentTime
chainSyncingPeer >>= \case
Just ChainSync {peer = p, timestamp = t}
| now `diffUTCTime` t > to -> do
$(logErrorS) "Chain" $
"Syncing peer timed out: " <> p.label
PeerTimeout `killPeer` p
| otherwise -> return ()
Nothing -> syncNewPeer
withSyncLoop ::
(MonadUnliftIO m, MonadLoggerIO m) =>
Chain ->
m a ->
m a
withSyncLoop ch f =
withAsync go $ \a ->
link a >> f
where
go = forever $ do
delay <-
liftIO $
randomRIO
( 2 * 1000 * 1000,
20 * 1000 * 1000
)
threadDelay delay
ChainPing `send` ch.mailbox
-- | Version of the database.
dataVersion :: Word32
dataVersion = 1
-- | Initialize header database. If version is different from current, the
-- database is purged of conflicting elements first.
initChainDB :: (MonadChain m) => m ()
initChainDB = do
db <- asks (.config.db)
mcf <- asks (.config.cf)
net <- asks (.config.net)
ver <- retrieveCommon db mcf ChainDataVersionKey
when (ver /= Just dataVersion) $ purgeChainDB >>= writeBatch db
case mcf of
Nothing -> insert db ChainDataVersionKey dataVersion
Just cf -> insertCF db cf ChainDataVersionKey dataVersion
retrieveCommon db mcf BestBlockKey >>= \b ->
when (isNothing (b :: Maybe BlockNode)) $
withBlockHeaders $ do
addBlockHeader (genesisNode net)
setBestBlockHeader (genesisNode net)
-- | Purge database of elements having keys that may conflict with those used in
-- this module.
purgeChainDB :: (MonadChain m) => m [R.BatchOp]
purgeChainDB = do
db <- asks (.config.db)
mcf <- asks (.config.cf)
f db mcf $ \it -> do
R.iterSeek it $ B.singleton 0x90
recurse_delete it db mcf
where
f db Nothing = R.withIter db
f db (Just cf) = R.withIterCF db cf
recurse_delete it db mcf =
R.iterKey it >>= \case
Just k
| B.head k == 0x90 || B.head k == 0x91 -> do
case mcf of
Nothing -> R.delete db k
Just cf -> R.deleteCF db cf k
R.iterNext it
(R.Del k :) <$> recurse_delete it db mcf
_ -> return []
-- | Import a bunch of continuous headers. Returns 'True' if the number of
-- headers is 2000, which means that there are possibly more headers to sync
-- from whatever peer delivered these.
importHeaders ::
(MonadChain m) =>
Network ->
UTCTime ->
[BlockHeader] ->
m (Either PeerException Bool)
importHeaders net now hs =
runExceptT $
lift connect >>= \case
Right _ -> do
case hs of
[] -> return ()
_ -> do
bb <- lift get_last
box <- asks (.state)
atomically . modifyTVar box $ \s ->
s {syncing = (\x -> x {best = bb}) <$> s.syncing}
case length hs of
2000 -> return False
_ -> return True
Left _ -> throwError PeerSentBadHeaders
where
timestamp = floor (utcTimeToPOSIXSeconds now)
connect = withBlockHeaders $ connectBlocks net timestamp hs
get_last = withBlockHeaders . getBlockHeader . headerHash $ last hs
-- | Check if best block header is in sync with the rest of the block chain by
-- comparing the best block with the current time, verifying that there are no
-- peers in the queue to be synced, and no peer is being synced at the moment.
-- This function will only return 'True' once. It should be used to decide
-- whether to notify other processes that the header chain has been synced. The
-- state of the chain will be flipped to synced when this function returns
-- 'True'.
notifySynced :: (MonadChain m) => m Bool
notifySynced =
fmap isJust $
runMaybeT $ do
bb <- lift $ withBlockHeaders getBestBlockHeader
now <- liftIO getCurrentTime
guard $ now `diffUTCTime` block_time bb > 7200
st <- asks (.state)
MaybeT . atomically . runMaybeT $ do
s <- lift $ readTVar st
guard $ isNothing s.syncing
guard $ null s.peers
guard $ not s.beenInSync
lift $ writeTVar st s {beenInSync = True}
return ()
where
block_time =
posixSecondsToUTCTime . fromIntegral . (.header.timestamp)
-- | Get next peer to sync against from the queue.
nextPeer :: (MonadChain m) => m (Maybe Peer)
nextPeer = do
ps <- (.peers) <$> (readTVarIO =<< asks (.state))
go ps
where
go [] = return Nothing
go (p : ps) =
setSyncingPeer p >>= \case
True -> return (Just p)
False -> go ps
-- | Set a syncing peer and generate a 'GetHeaders' data structure with a block
-- locator to send to that peer for syncing.
syncHeaders ::
(MonadChain m) =>
UTCTime ->
BlockNode ->
Peer ->
m GetHeaders
syncHeaders now bb p = do
st <- asks (.state)
atomically $
modifyTVar st $ \s ->
s
{ syncing =
Just
ChainSync
{ peer = p,
timestamp = now,
best = Nothing
},
peers = delete p s.peers
}
loc <- withBlockHeaders $ blockLocator bb
return
GetHeaders
{ version = myVersion,
locator = loc,
stop = z
}
where
z = "0000000000000000000000000000000000000000000000000000000000000000"
-- | Set the time of last received data to now if a syncing peer is active.
setLastReceived :: (MonadChain m) => m ()
setLastReceived = do
now <- liftIO getCurrentTime
st <- asks (.state)
let f ChainSync {..} = ChainSync {timestamp = now, ..}
atomically . modifyTVar st $ \s ->
s {syncing = f <$> s.syncing}
-- | Add a new peer to the queue of peers to sync against.
addPeer :: (MonadChain m) => Peer -> m ()
addPeer p = do
st <- asks (.state)
atomically . modifyTVar st $ \s -> s {peers = nub (p : s.peers)}
-- | Get syncing peer if there is one.
getSyncingPeer :: (MonadChain m) => m (Maybe Peer)
getSyncingPeer =
fmap (.peer) . (.syncing)
<$> (readTVarIO =<< asks (.state))
setSyncingPeer :: (MonadChain m) => Peer -> m Bool
setSyncingPeer p =
setBusy p >>= \case
False -> do
$(logDebugS) "Chain" $
"Could not lock peer: " <> p.label
return False
True -> do
$(logDebugS) "Chain" $
"Locked peer: " <> p.label
set_it
return True
where
set_it = do
now <- liftIO getCurrentTime
box <- asks (.state)
atomically $ modifyTVar box $ \s ->
s
{ syncing =
Just
ChainSync
{ peer = p,
timestamp = now,
best = Nothing
}
}
-- | Remove a peer from the queue of peers to sync and unset the syncing peer if
-- it is set to the provided peer.
finishPeer :: (MonadChain m) => Peer -> m ()
finishPeer p =
asks (.state) >>= remove_peer >>= \case
False ->
$(logDebugS) "Chain" $
"Removed peer from queue: " <> p.label
True -> do
$(logDebugS) "Chain" $
"Releasing syncing peer: " <> p.label
setFree p
where
remove_peer st =
atomically $
readTVar st >>= \s -> case s.syncing of
Just ChainSync {peer = p'}
| p == p' -> do
unset_syncing st
return True
_ -> do
remove_from_queue st
return False
unset_syncing st =
modifyTVar st $ \x ->
x {syncing = Nothing}
remove_from_queue st =
modifyTVar st $ \x ->
x {peers = delete p x.peers}
-- | Return syncing peer data.
chainSyncingPeer :: (MonadChain m) => m (Maybe ChainSync)
chainSyncingPeer =
(.syncing) <$> (readTVarIO =<< asks (.state))
-- | Get a block header from 'Chain' process.
chainGetBlock ::
(MonadIO m) =>
BlockHash ->
Chain ->
m (Maybe BlockNode)
chainGetBlock bh ch =
runReaderT (getBlockHeader bh) (ch.reader.config)
-- | Get best block header from chain process.
chainGetBest :: (MonadIO m) => Chain -> m BlockNode
chainGetBest ch =
runReaderT getBestBlockHeader ch.reader.config
-- | Get ancestor of 'BlockNode' at 'BlockHeight' from chain process.
chainGetAncestor ::
(MonadIO m) =>
BlockHeight ->
BlockNode ->
Chain ->
m (Maybe BlockNode)
chainGetAncestor h bn ch =
runReaderT (getAncestor h bn) ch.reader.config
-- | Get parents of 'BlockNode' starting at 'BlockHeight' from chain process.
chainGetParents ::
(MonadIO m) =>
BlockHeight ->
BlockNode ->
Chain ->
m [BlockNode]
chainGetParents height top ch =
go [] top
where
go acc b
| height >= b.height = return acc
| otherwise = do
m <- chainGetBlock b.header.prev ch
case m of
Nothing -> return acc
Just p -> go (p : acc) p
-- | Get last common block from chain process.
chainGetSplitBlock ::
(MonadIO m) =>
BlockNode ->
BlockNode ->
Chain ->
m BlockNode
chainGetSplitBlock l r ch =
runReaderT (splitPoint l r) ch.reader.config
-- | Notify chain that a new peer is connected.
chainPeerConnected ::
(MonadIO m) =>
Peer ->
Chain ->
m ()
chainPeerConnected p ch =
ChainPeerConnected p `send` ch.mailbox
-- | Notify chain that a peer has disconnected.
chainPeerDisconnected ::
(MonadIO m) =>
Peer ->
Chain ->
m ()
chainPeerDisconnected p ch =
ChainPeerDisconnected p `send` ch.mailbox
-- | Is given 'BlockHash' in the main chain?
chainBlockMain ::
(MonadIO m) =>
BlockHash ->
Chain ->
m Bool
chainBlockMain bh ch =
chainGetBest ch >>= \bb ->
chainGetBlock bh ch >>= \case
Nothing ->
return False
bm@(Just bn) ->
(== bm) <$> chainGetAncestor bn.height bb ch
-- | Is chain in sync with network?
chainIsSynced :: (MonadIO m) => Chain -> m Bool
chainIsSynced ch =
(.beenInSync) <$> readTVarIO (ch.reader.state)
-- | Peer sends a bunch of headers to the chain process.
chainHeaders ::
(MonadIO m) =>
Peer ->
[BlockHeader] ->
Chain ->
m ()
chainHeaders p hs ch =
ChainHeaders p hs `send` ch.mailbox