packages feed

haskoin-node-0.2.0: Network/Haskoin/Node/HeaderTree.hs

module Network.Haskoin.Node.HeaderTree
( HeaderTree(..)
, BlockHeaderNode(..)
, BlockChainAction(..)
, BlockHeight
, Timestamp
, initHeaderTree
, connectHeader
, connectHeaders
, commitAction
, isBestChain
, isChainReorg
, isSideChain
, isKnownChain
, blockLocator
, blockLocatorHeight
, blockLocatorSide
, blockLocatorPartial
, getNodeWindow
, bestBlockHeaderHeight
, getBlockHeaderHeight
, getNodeAtTimestamp
, genesisNode
, getParentNode
) where

import Control.Monad (foldM, when, unless, liftM, (<=<), forM, forM_)
import Control.Monad.Trans (lift, liftIO, MonadIO)
import Control.Monad.Trans.Either (EitherT, left, runEitherT)
import Control.Monad.Reader (MonadReader(..), ReaderT, ask)
import Control.DeepSeq (NFData(..))

import Data.Word (Word32)
import Data.Bits (shiftL)
import Data.Maybe (fromMaybe, isNothing, isJust, catMaybes)
import Data.List (sort)
import Data.Binary.Get (getWord32le)
import Data.Binary.Put (putWord32le)
import Data.Default (def)
import qualified Data.Binary as B (Binary, get, put)
import qualified Data.ByteString as BS (ByteString, reverse, append)

import qualified Database.LevelDB.Base as L (DB, get, put)

import Network.Haskoin.Block
import Network.Haskoin.Constants
import Network.Haskoin.Util
import Network.Haskoin.Node.Checkpoints

type BlockHeight = Word32
type Timestamp = Word32

class Monad m => HeaderTree m where
    getBlockHeaderNode     :: BlockHash -> m (Maybe BlockHeaderNode)
    putBlockHeaderNode     :: BlockHeaderNode -> m ()
    -- The height is only updated when the node is part of the main chain.
    -- Side chains are not indexed by their height.
    putBlockHeaderHeight   :: BlockHeaderNode -> m ()
    getBlockHeaderByHeight :: BlockHeight -> m (Maybe BlockHeaderNode)
    getBestBlockHeader     :: m BlockHeaderNode
    setBestBlockHeader     :: BlockHeaderNode -> m ()

-- | Data type representing a BlockHeader node in the header chain. It
-- contains additional data such as the chain work and chain height for this
-- node.
data BlockHeaderNode = BlockHeaderNode
    { nodeBlockHash    :: !BlockHash
    , nodeHeader       :: !BlockHeader
    , nodeHeaderHeight :: !BlockHeight
    , nodeChainWork    :: !Integer
    , nodeChild        :: !(Maybe BlockHash)
    , nodeMedianTimes  :: ![Timestamp]
    , nodeMinWork      :: !Word32 -- Only used for testnet
    } deriving (Show, Read, Eq)

instance NFData BlockHeaderNode where
    rnf BlockHeaderNode{..} =
        rnf nodeBlockHash `seq`
        rnf nodeHeader `seq`
        rnf nodeHeaderHeight `seq`
        rnf nodeChainWork `seq`
        rnf nodeChild `seq`
        rnf nodeMedianTimes `seq`
        rnf nodeMinWork

instance B.Binary BlockHeaderNode where

    get = do
        nodeBlockHash    <- B.get
        nodeHeader       <- B.get
        nodeHeaderHeight <- getWord32le
        nodeChainWork    <- B.get
        nodeChild        <- B.get
        nodeMedianTimes  <- B.get
        nodeMinWork      <- B.get
        return BlockHeaderNode{..}

    put BlockHeaderNode{..} = do
        B.put       nodeBlockHash
        B.put       nodeHeader
        putWord32le nodeHeaderHeight
        B.put       nodeChainWork
        B.put       nodeChild
        B.put       nodeMedianTimes
        B.put       nodeMinWork

data BlockChainAction
    = BestChain  { actionNodes     :: ![BlockHeaderNode] }
    | ChainReorg { actionSplitNode :: !BlockHeaderNode
                 , actionOldNodes  :: ![BlockHeaderNode]
                 , actionNodes     :: ![BlockHeaderNode]
                 }
    | SideChain  { actionNodes     :: ![BlockHeaderNode] }
    | KnownChain { actionNodes     :: ![BlockHeaderNode] }
    deriving (Read, Show, Eq)

instance NFData BlockChainAction where
    rnf bca = case bca of
        BestChain ns -> rnf ns
        ChainReorg s os ns -> rnf s `seq` rnf os `seq` rnf ns
        KnownChain ns -> rnf ns
        SideChain ns -> rnf ns

-- | Returns True if the action is a best chain
isBestChain :: BlockChainAction -> Bool
isBestChain (BestChain _) = True
isBestChain _             = False

-- | Returns True if the action is a chain reorg
isChainReorg :: BlockChainAction -> Bool
isChainReorg ChainReorg{} = True
isChainReorg _            = False

-- | Returns True if the action is a side chain
isSideChain :: BlockChainAction -> Bool
isSideChain (SideChain _) = True
isSideChain _             = False

-- | Returns True if the action is a known chain
isKnownChain :: BlockChainAction -> Bool
isKnownChain (KnownChain _) = True
isKnownChain _              = False

-- | Number of blocks on average between difficulty cycles (2016 blocks)
diffInterval :: Word32
diffInterval = targetTimespan `div` targetSpacing

-- | Genesis BlockHeaderNode
genesisNode :: BlockHeaderNode
genesisNode = BlockHeaderNode
    { nodeBlockHash    = headerHash genesisHeader
    , nodeHeader       = genesisHeader
    , nodeHeaderHeight = 0
    , nodeChainWork    = headerWork genesisHeader
    , nodeChild        = Nothing
    , nodeMedianTimes  = [blockTimestamp genesisHeader]
    , nodeMinWork      = blockBits genesisHeader
    }

-- | Initialize the block header chain by inserting the genesis block if
-- it doesn't already exist.
initHeaderTree :: HeaderTree m => m ()
initHeaderTree = do
    genM <- getBlockHeaderNode $ nodeBlockHash genesisNode
    when (isNothing genM) $ do
        putBlockHeaderNode genesisNode
        putBlockHeaderHeight genesisNode
        setBestBlockHeader genesisNode

-- A more efficient way of connecting a list of BlockHeaders than connecting
-- them individually. The work check will only be done once for the whole
-- chain. The list of BlockHeaders have to form a valid chain, linked by their
-- parents.
connectHeaders :: HeaderTree m
               => [BlockHeader]
               -> Timestamp
               -> Bool
               -> m (Either String BlockChainAction)
connectHeaders bhs adjustedTime commit
    | null bhs = return $ Left "Invalid empty BlockHeaders in connectHeaders"
    | validChain bhs = runEitherT $ do
        newNodes <- forM bhs $ \bh -> do
            parNode <- verifyBlockHeader bh adjustedTime
            lift $ storeBlockHeader bh parNode
        -- Best header will only be updated if we have no errors
        lift $ evalNewChain commit newNodes
    | otherwise = return $ Left "BlockHeaders do not form a valid chain."
  where
    validChain (a:b:xs) =  prevBlock b == headerHash a && validChain (b:xs)
    validChain [_] = True
    validChain _ = False

-- | Connect a block header to this block header chain. Corresponds to bitcoind
-- function ProcessBlockHeader and AcceptBlockHeader in main.cpp.
connectHeader :: HeaderTree m
              => BlockHeader
              -> Timestamp
              -> Bool
              -> m (Either String BlockChainAction)
connectHeader bh adjustedTime commit = runEitherT $ do
    parNode <- verifyBlockHeader bh adjustedTime
    lift $ evalNewChain commit . (: []) =<< storeBlockHeader bh parNode

evalNewChain :: HeaderTree m
             => Bool
             -> [BlockHeaderNode]
             -> m BlockChainAction
evalNewChain commit newNodes = do
    currentHead <- getBestBlockHeader
    action <- go =<< findSplitNode currentHead (last newNodes)
    when commit $ commitAction action
    return action
  where
    go (split, old, new)
        | null old && not (null new) = return $ BestChain new
        | not (null old) && not (null new) =
            return $ if nodeChainWork (last new) > nodeChainWork (last old)
                  then ChainReorg split old new
                  else SideChain $ split : new
        | otherwise = return $ KnownChain newNodes

-- | Update the best block header of the action in the header tree
commitAction :: HeaderTree m => BlockChainAction -> m ()
commitAction action = do
    currentHead <- getBestBlockHeader
    case action of
        BestChain nodes -> unless (null nodes) $ do
            updateChildren $ currentHead:nodes
            forM_ nodes putBlockHeaderHeight
            setBestBlockHeader $ last nodes
        ChainReorg s _ ns -> unless (null ns) $ do
            updateChildren $ s:ns
            forM_ ns putBlockHeaderHeight
            setBestBlockHeader $ last ns
        KnownChain _ -> return ()
        SideChain _ -> return ()
  where
    updateChildren (a:b:xs) = do
        putBlockHeaderNode a{ nodeChild = Just $ nodeBlockHash b }
        updateChildren (b:xs)
    updateChildren _ = return ()

-- TODO: Add DOS return values
verifyBlockHeader :: HeaderTree m
                  => BlockHeader
                  -> Timestamp
                  -> EitherT String m BlockHeaderNode
verifyBlockHeader bh adjustedTime  = do
    unless (isValidPOW bh) $ left "Invalid proof of work"

    unless (blockTimestamp bh <= adjustedTime + 2 * 60 * 60) $
        left "Invalid header timestamp"

    parNodeM <- lift $ getBlockHeaderNode $ prevBlock bh
    parNode <- maybe (left "Parent block not found") return parNodeM

    nextWork <- lift $ nextWorkRequired parNode bh
    unless (blockBits bh == nextWork) $ left "Incorrect work transition (bits)"

    let sortedMedians = sort $ nodeMedianTimes parNode
        medianTime    = sortedMedians !! (length sortedMedians `div` 2)
    when (blockTimestamp bh <= medianTime) $ left "Block timestamp is too early"

    chkPointM <- lift lastSeenCheckpoint
    let newHeight = nodeHeaderHeight parNode + 1
    unless (maybe True ((fromIntegral newHeight >) . fst) chkPointM) $
        left "Rewriting pre-checkpoint chain"

    unless (verifyCheckpoint (fromIntegral newHeight) bid) $
        left "Rejected by checkpoint lock-in"

    -- All block of height 227836 or more use version 2 in prodnet
    -- TODO: Find out the value here for testnet
    when (  networkName == "prodnet"
         && blockVersion bh == 1
         && nodeHeaderHeight parNode + 1 >= 227836) $
        left "Rejected version=1 block"

    return parNode
  where
    bid = headerHash bh

-- Build a new block header and store it
storeBlockHeader :: HeaderTree m
                 => BlockHeader
                 -> BlockHeaderNode
                 -> m BlockHeaderNode
storeBlockHeader bh parNode = do
    let nodeBlockHash    = bid
        nodeHeader       = bh
        nodeHeaderHeight = newHeight
        nodeChainWork    = newWork
        nodeChild        = Nothing
        nodeMedianTimes  = newMedian
        nodeMinWork      = minWork
        newNode          = BlockHeaderNode{..}
    prevM <- getBlockHeaderNode bid
    case prevM of
        Just prev -> return prev
        Nothing   -> putBlockHeaderNode newNode >> return newNode
  where
    bid       = headerHash bh
    newHeight = nodeHeaderHeight parNode + 1
    newWork   = nodeChainWork parNode + headerWork bh
    newMedian = blockTimestamp bh : take 10 (nodeMedianTimes parNode)
    isDiffChange = newHeight `mod` diffInterval == 0
    isNotLimit   = blockBits bh /= encodeCompact powLimit
    minWork | not allowMinDifficultyBlocks = 0
            | isDiffChange || isNotLimit   = blockBits bh
            | otherwise                    = nodeMinWork parNode

-- | Return the window of nodes starting from the child of the given node.
-- Child links are followed to build the window. If the window ends in an
-- orphaned chain, we backtrack and return the window in the main chain.
-- The result is returned in a BlockChainAction to know if we had to
-- backtrack into the main chain or not.
getNodeWindow :: HeaderTree m
              => BlockHash -> Int -> m (Maybe BlockChainAction)
getNodeWindow bh cnt = getBlockHeaderNode bh >>= \nodeM -> case nodeM of
        Just node -> go [] cnt node
        Nothing -> return Nothing
  where
    go [] 0 _  = return Nothing
    go acc 0 _ = return $ Just $ BestChain $ reverse acc
    go acc i node = getChildNode node >>= \childM -> case childM of
        Just child -> go (child:acc) (i-1) child
        Nothing -> do
            -- We are at the end of our chain. Check if there is a better chain.
            currentHead <- getBestBlockHeader
            if nodeChainWork currentHead > nodeChainWork node
                -- We got stuck in an orphan chain. We need to backtrack.
                then findMainChain currentHead
                -- We are at the end of the main chain
                else return $ if null acc
                    then Nothing
                    else Just $ BestChain $ reverse acc
    findMainChain currentHead = do
        -- Compute the split point from the original input node so that the old
        -- chain doesn't contain blocks beyond the original node.
        node <- fromMaybe e <$> getBlockHeaderNode bh
        (split, old, new) <- findSplitNode node currentHead
        return $ Just $ ChainReorg split old $ take cnt new
    e = error "getNodeWindow: Could not get block header node"

-- Find the first node right after the given timestamp
getNodeAtTimestamp :: HeaderTree m
                   => Timestamp
                   -> m (Maybe BlockHeaderNode)
getNodeAtTimestamp ts = do
    best <- getBestBlockHeader
    if ts < blockTimestamp (nodeHeader best)
        -- there is a solution. Perform a binary search.
        then go 0 $ nodeHeaderHeight best
        -- There is no solution.
        else return Nothing
  where
    go a b
        | a == b = getBlockHeaderByHeight a
        | otherwise = do
            -- compute the middle point
            let m = a + ((b - a) `div` 2)
            nodeM <- getBlockHeaderByHeight m
            case nodeM of
                Just node -> if ts < blockTimestamp (nodeHeader node)
                    -- The block is after the timestamp. The block could be the
                    -- solution, or the solution is smaller.
                    then go a m
                    -- The block is before the timestamp. The solution is
                    -- striclty higher than m.
                    else go (m+1) b
                _ -> error $ unwords
                    [ "getNodeAtTimestamp: Possibly corrupted chain"
                    , "at height", show m
                    ]

-- | Find the split point between two nodes. It also returns the two partial
-- chains leading from the split point to the respective nodes.
findSplitNode :: HeaderTree m
              => BlockHeaderNode
              -> BlockHeaderNode
              -> m (BlockHeaderNode, [BlockHeaderNode], [BlockHeaderNode])
findSplitNode =
    go [] []
  where
    go xs ys x y
        | nodeBlockHash x == nodeBlockHash y = return (x, xs, ys)
        | nodeHeaderHeight x > nodeHeaderHeight y = do
            par <- fromMaybe e <$> getParentNode x
            go (x:xs) ys par y
        | otherwise = do
            par <- fromMaybe e <$> getParentNode y
            go xs (y:ys) x par
    e = error "findSplitNode: Could not get parent node"

-- | Finds the parent of a BlockHeaderNode
getParentNode :: HeaderTree m => BlockHeaderNode -> m (Maybe BlockHeaderNode)
getParentNode node
    | p == z    = return Nothing
    | otherwise = getBlockHeaderNode p
  where
    p = prevBlock $ nodeHeader node
    z = "0000000000000000000000000000000000000000000000000000000000000000"

-- | Finds the child of a BlockHeaderNode if it exists. If a node has
-- multiple children, this function will always return the child on the
-- main branch.
getChildNode :: HeaderTree m => BlockHeaderNode -> m (Maybe BlockHeaderNode)
getChildNode node = case nodeChild node of
    Just child -> getBlockHeaderNode child
    Nothing    -> return Nothing

-- | Get the last checkpoint that we have seen
lastSeenCheckpoint :: HeaderTree m => m (Maybe (Int, BlockHash))
lastSeenCheckpoint =
    go $ reverse checkpointList
  where
    go ((i, chk):xs) = do
        existsChk <- liftM isJust $ getBlockHeaderNode chk
        if existsChk then return $ Just (i, chk) else go xs
    go [] = return Nothing

-- | Returns the work required for a BlockHeader given the previous
-- BlockHeaderNode. This function coresponds to bitcoind function
-- GetNextWorkRequired in main.cpp.
nextWorkRequired :: HeaderTree m => BlockHeaderNode -> BlockHeader -> m Word32
nextWorkRequired lastNode bh
    -- Genesis block
    | prevBlock (nodeHeader lastNode) == z = return $ encodeCompact powLimit
    -- Only change the difficulty once per interval
    | (nodeHeaderHeight lastNode + 1) `mod` diffInterval /= 0 = return $
        if allowMinDifficultyBlocks
            then minPOW
            else blockBits $ nodeHeader lastNode
    | otherwise = do
        -- TODO: Can this break if there are not enough blocks in the chain?
        firstNode <- foldM (flip ($)) lastNode fs
        let lastTs = blockTimestamp $ nodeHeader firstNode
        return $ workFromInterval lastTs (nodeHeader lastNode)
  where
    len   = fromIntegral diffInterval - 1
    fs    = replicate len (liftM (fromMaybe e) . getParentNode)
    e = error "nextWorkRequired: Could not get parent node"
    delta = targetSpacing * 2
    minPOW
        | blockTimestamp bh > blockTimestamp (nodeHeader lastNode) + delta =
            encodeCompact powLimit
        | otherwise = nodeMinWork lastNode
    z = "0000000000000000000000000000000000000000000000000000000000000000"

-- | Computes the work required for the next block given a timestamp and the
-- current block. The timestamp should come from the block that matched the
-- last jump in difficulty (spaced out by 2016 blocks in prodnet).
workFromInterval :: Timestamp -> BlockHeader -> Word32
workFromInterval ts lastB
    | newDiff > powLimit = encodeCompact powLimit
    | otherwise          = encodeCompact newDiff
  where
    t = fromIntegral $ blockTimestamp lastB - ts
    actualTime
        | t < targetTimespan `div` 4 = targetTimespan `div` 4
        | t > targetTimespan * 4     = targetTimespan * 4
        | otherwise                  = t
    lastDiff = decodeCompact $ blockBits lastB
    newDiff = lastDiff * toInteger actualTime `div` toInteger targetTimespan

-- | Returns a BlockLocator object (newest block first, genesis at the end)
blockLocator :: HeaderTree m => m BlockLocator
blockLocator = bestBlockHeaderHeight >>= blockLocatorHeight

-- | Returns a BlockLocator object for a given block height on the main chain.
blockLocatorHeight :: HeaderTree m => BlockHeight -> m BlockLocator
blockLocatorHeight height = do
    -- Take only indices > 0 to avoid the genesis block
    let is = takeWhile (> (0 :: Int)) $
            [h, (h - 1) .. (h - 9)] ++ [(h - 10) - 2^x | x <- [(0 :: Int)..]]
    ns <- liftM catMaybes $ forM (map fromIntegral is) getBlockHeaderByHeight
    return $ map nodeBlockHash ns ++ [headerHash genesisHeader]
  where
    h = fromIntegral height

-- | Build a block locator starting from a side chain. We do not have access
-- to the height index for the portion of the side chain so we build the
-- part in the side chain manually, then use the block height index for the
-- rest of the locator once we're back on the main chain.
blockLocatorSide :: HeaderTree m => BlockChainAction -> m BlockLocator
blockLocatorSide action = case action of
    SideChain (split:nodes) -> do
        mainLoc <- blockLocatorHeight $ nodeHeaderHeight split
        return $ take 10 (map nodeBlockHash $ reverse nodes) ++ mainLoc
    _ -> error "blockLocatorSide: Invalid blockchain action provided"

-- | Returns a partial BlockLocator object.
blockLocatorPartial :: HeaderTree m => Int -> m BlockLocator
blockLocatorPartial i
    | i < 1 = error "Locator length must be greater than 0"
    | otherwise = do
        h <- getBestBlockHeader
        liftM (map nodeBlockHash . reverse) $ go [] i h
  where
    go acc 1 node = return $ node:acc
    go acc step node = getParentNode node >>= \parM -> case parM of
        Just par -> go (node:acc) (step - 1) par
        Nothing  -> return $ node:acc

bestBlockHeaderHeight :: HeaderTree m => m BlockHeight
bestBlockHeaderHeight = liftM nodeHeaderHeight getBestBlockHeader

getBlockHeaderHeight :: HeaderTree m => BlockHash -> m (Maybe BlockHeight)
getBlockHeaderHeight = return . fmap nodeHeaderHeight <=< getBlockHeaderNode

-- | Returns True if the difficulty target (bits) of the header is valid
-- and the proof of work of the header matches the advertised difficulty target.
-- This function corresponds to the function CheckProofOfWork from bitcoind
-- in main.cpp
isValidPOW :: BlockHeader -> Bool
isValidPOW bh
    | target <= 0 || target > powLimit = False
    | otherwise = headerPOW bh <= fromIntegral target
  where
    target = decodeCompact $ blockBits bh

-- | Returns the proof of work of a block header as an Integer number.
headerPOW :: BlockHeader -> Integer
headerPOW =  bsToInteger . BS.reverse . encode' . headerHash

-- | Returns the work represented by this block. Work is defined as the number
-- of tries needed to solve a block in the average case with respect to the
-- target.
headerWork :: BlockHeader -> Integer
headerWork bh =
    largestHash `div` (target + 1)
  where
    target      = decodeCompact (blockBits bh)
    largestHash = 1 `shiftL` 256

{- Default LevelDB implementation -}

blockHashKey :: BlockHash -> BS.ByteString
blockHashKey bid = "b_" `BS.append` encode' bid

bestBlockKey :: BS.ByteString
bestBlockKey = "bestblockheader_"

heightKey :: BlockHeight -> BS.ByteString
heightKey h = "h_" `BS.append` encode' h

-- Get a node which is directly referenced by the key
getLevelDBNode :: MonadIO m
               => BS.ByteString -> ReaderT L.DB m (Maybe BlockHeaderNode)
getLevelDBNode key = do
    db <- ask
    resM <- liftIO $ L.get db def key
    return $ decodeToMaybe =<< resM

-- Get a node that has 1 level of indirection
getLevelDBNode' :: MonadIO m
                => BS.ByteString -> ReaderT L.DB m (Maybe BlockHeaderNode)
getLevelDBNode' key = do
    db <- ask
    resM <- liftIO $ L.get db def key
    maybe (return Nothing) getLevelDBNode resM

instance MonadIO m => HeaderTree (ReaderT L.DB m) where
    getBlockHeaderNode = getLevelDBNode . blockHashKey
    putBlockHeaderNode node = do
        db <- ask
        liftIO $ L.put db def (blockHashKey $ nodeBlockHash node) $ encode' node
    getBlockHeaderByHeight = getLevelDBNode' . heightKey
    putBlockHeaderHeight node = do
        db <- ask
        let val = blockHashKey $ nodeBlockHash node
        liftIO $ L.put db def (heightKey $ nodeHeaderHeight node) val
    getBestBlockHeader = do
        db <- ask
        keyM <- liftIO $ L.get db def bestBlockKey
        case keyM of
            Just key -> fromMaybe e <$> getLevelDBNode key
            Nothing  -> error
                "GetBestBlockHeader: Best block header does not exist"
      where
        e = error "getBestBlockHeader: Could not get LevelDB node"
    setBestBlockHeader node = do
        db <- ask
        liftIO $ L.put db def bestBlockKey $ blockHashKey $ nodeBlockHash node