bitcoin-hs-0.0.1: Bitcoin/BlockChain/TxLookup.hs
-- | An simple but relatively compact data structure for looking up transactions in the blockchain
-- (TODO: replace these data structures by a better one...)
--
-- Blockchain stats (as of 2013):
--
-- * block heigh currently ~270,000, so 20 bits for that is enough for a while, 23 enough for ever basically.
--
-- * number of transaction is currently ~27,000,000 (2016 data: ~162 million)
--
-- * average number of transactions per block is currently ~300-350 (2016: ~2000)
--
-- Idea: index into a large dense array by the first few (say 24 or 26) bits of the hash,
-- then store a list (vector) of the possible block indices there. With 24 bit index there will be in
-- average 2 blocks per tx, of course sometimes more sometimes less.
--
-- An IOArray -> 16M pointers, on 32 bit that is 64M
-- 30 million entries tx-s => 30 million entries, for short lists approx 1 word/entry, so let's say 150-200M on 32 bit.
--
-- On 64 bit we have more memory so it's ok :)
--
-- 2016 update: you will need more memory to store even 32 bits per transaction...
-- The most compact estimation already gives 1+ gigs...
--
--
{-# LANGUAGE UnboxedTuples, MagicHash, BangPatterns #-}
module Bitcoin.BlockChain.TxLookup where
--------------------------------------------------------------------------------
import Control.Monad
import Data.Bits
import Data.Word
import Data.List ( find )
import Data.Maybe
import Foreign
import System.IO.Unsafe as Unsafe
import GHC.Prim
import GHC.Exts
import GHC.Int
import Data.Array
import Data.Array.IO
import Data.Array.MArray
import Data.Array.Unsafe
import Control.DeepSeq
-- import GHC.DataSize
import Bitcoin.Protocol.Hash
import Bitcoin.BlockChain.Base
import Bitcoin.BlockChain.Load
import Bitcoin.BlockChain.Chain
import Bitcoin.BlockChain.Tx
import Bitcoin.Script.Base
import Bitcoin.Script.Run ( checkTransaction )
--------------------------------------------------------------------------------
-- * interal stuff
{-
newtype Word24 = Word24 { fromWord24 :: Word32 } deriving (Eq,Ord,Show)
instance Storable Word24 where
alignment _ = 1
sizeOf _ = 3
peek ptr = do
lo <- peek (castPtr ptr :: Ptr Word16)
hi <- peek (castPtr (ptr `plusPtr` 2) :: Ptr Word8 ) -- little endian
return $ Word24 $ fromIntegral lo + shiftL (fromIntegral hi) 16
poke ptr (Word24 w) = do
let lo = fromIntegral (w .&. 0xffff) :: Word16
hi = fromIntegral (shiftR w 16) :: Word8
poke (castPtr ptr :: Ptr Word16) lo
poke (castPtr (ptr `plusPtr` 2) :: Ptr Word8 ) hi -- little endian
highestBit :: Word24 -> Bool
highestBit (Word24 w) = (w .&. 0x800000) > 0
lowerBits :: Word24 -> Int
lowerBits (Word24 w) = fromIntegral (w .&. 0x7fffff)
minus1 :: Word24
minus1 = Word24 0xffffff
-}
--------------------------------------------------------------------------------
toWord# :: Int -> Word#
toWord# !(I# i) = int2Word# i
fromWord# :: Word# -> Int
fromWord# w = I# (word2Int# w)
-- | A list which is more compact for a small number of elements
data CompactList
= NilList
| OneList !Word#
| TwoList !Word# !Word#
| ThrList !Word# !Word# !Word#
| FouList !Word# !Word# !Word# !Word#
| GenList !IndexList
instance NFData CompactList where
rnf cl = case cl of
NilList -> ()
OneList _ -> ()
TwoList _ _ -> ()
ThrList _ _ _ -> ()
FouList _ _ _ _ -> ()
GenList il -> rnf il
toCompactList :: [Int] -> CompactList
toCompactList xs = case xs of
[] -> NilList
[x] -> OneList (toWord# x)
[x,y] -> TwoList (toWord# x) (toWord# y)
[x,y,z] -> ThrList (toWord# x) (toWord# y) (toWord# z)
[x,y,z,w] -> FouList (toWord# x) (toWord# y) (toWord# z) (toWord# w)
_ -> GenList (toIndexList xs)
fromCompactList :: CompactList -> [Int]
fromCompactList cl = case cl of
NilList -> []
OneList u -> [ fromWord# u ]
TwoList u v -> [ fromWord# u , fromWord# v ]
ThrList u v w -> [ fromWord# u , fromWord# v , fromWord# w ]
FouList u v w z -> [ fromWord# u , fromWord# v , fromWord# w , fromWord# z ]
GenList il -> fromIndexList il
consCompactList :: Int -> CompactList -> CompactList
consCompactList x rest = case rest of
NilList -> OneList (toWord# x)
OneList y -> TwoList (toWord# x) y
TwoList y z -> ThrList (toWord# x) y z
ThrList y z w -> FouList (toWord# x) y z w
FouList {} -> GenList $ toIndexList $ (x:) $ fromCompactList rest
GenList il -> GenList $ consIndexList x il
--------------------------------------------------------------------------------
-- | Hack for a more compact list structure. Last element of the list has the highest bit set to 1.
-- Empty list has all bits set to 1. Space consumption is 3 words per list entry instead of 5 for normal lists.
--
-- Note: compiling with -O1 seeems to create larger memory consumption than -O0 and -O2 ?!?!
data IndexList = IndexList !Word# IndexList
instance NFData IndexList where
rnf (IndexList !w rest) = (if not (isNullIndexList rest) then rnf rest else ()) `seq` ()
intToBool# :: Int# -> Bool
intToBool# i = case i of
0# -> False
_ -> True
nullIndexList :: IndexList
nullIndexList = IndexList (int2Word# 0xffffffff#) nullIndexList
isNullIndexList :: IndexList -> Bool
isNullIndexList (IndexList w _) = intToBool# (w `eqWord#` (int2Word# 0xffffffff#))
isSingletonIndexList :: IndexList -> Bool
isSingletonIndexList (IndexList w _) = not $ intToBool# ((w `and#` (int2Word# 0x80000000#)) `eqWord#` (int2Word# 0#))
--------------------------------------------------------------------------------
consIndexList :: Int -> IndexList -> IndexList
consIndexList x rest =
if isNullIndexList rest
then IndexList (w0 `or#` (int2Word# 0x80000000#)) rest
else IndexList w0 rest
where
!(W# w0) = fromIntegral x
toIndexList :: [Int] -> IndexList
toIndexList = go where
go (x : xs) = IndexList w (go xs) where
!w = (w0 `or#` (if null xs then (int2Word# 0x80000000#) else (int2Word# 0#)))
!(W# w0) = fromIntegral x
go [] = nullIndexList
fromIndexList :: IndexList -> [Int]
fromIndexList il@(IndexList !w rest) =
if isNullIndexList il
then []
else go il
where
go (IndexList w rest) = if intToBool# ((w `and#` (int2Word# 0x80000000#)) `eqWord#` (int2Word# 0#))
then (fromIntegral $ W# (w )) : go rest
else (fromIntegral $ W# (w `and#` (int2Word# 0x7fffffff#))) : []
--------------------------------------------------------------------------------
newtype TxLookup = TxLookup (IOArray Word32 CompactList)
newEmptyTxLookup :: IO TxLookup
newEmptyTxLookup = do
arr <- Data.Array.MArray.newArray (0,0xffffff) NilList
return $ TxLookup arr
insertIntoTxLookup' :: Word32 -> Int -> TxLookup -> IO ()
insertIntoTxLookup' !key_ !value (TxLookup !arr) = do
let !key = key_ .&. 0x00ffffff -- 24 bits
!old <- readArray arr key
unless (elem value $ fromCompactList old) $ writeArray arr key $! consCompactList value old
return ()
txLookupList' :: Word32 -> TxLookup -> IO [Int]
txLookupList' key_ (TxLookup arr) = do
let key = key_ .&. 0x00ffffff -- 24 bits
list <- readArray arr key
return $ fromCompactList list
--------------------------------------------------------------------------------
{- ForeignPtr version
first24Bits :: Hash256 -> Word32
first24Bits hash = Unsafe.unsafePerformIO (first24BitsIO hash)
-- | Depends on the endianness, but it's used for a memory only structure anyway
first24BitsIO :: Hash256 -> IO Word32
first24BitsIO (Hash256 fptr) = do
w32 <- (withForeignPtr fptr $ \ptr -> peek (castPtr ptr :: Ptr Word32))
return (w32 .&. 0xffffff)
-}
first24Bits :: Hash256 -> Word32
first24Bits (Hash256 !w1 _ _ _) = fromIntegral (w1 .&. 0xffffff)
--------------------------------------------------------------------------------
insertIntoTxLookup :: Hash256 -> Int -> TxLookup -> IO ()
insertIntoTxLookup !hash !blockidx !table = insertIntoTxLookup' (first24Bits hash) blockidx table
txLookupList :: Hash256 -> TxLookup -> IO [Int]
txLookupList !hash !table = txLookupList' (first24Bits hash) table
--------------------------------------------------------------------------------
-- * Build and use the 'TxLookup' table
-- | Builds a 'TxLookup' table
buildTxLookupTable :: ChainTable -> IO TxLookup
buildTxLookupTable chTable = do
txlkptable <- newEmptyTxLookup
let (a,b) = bounds (_tableLongest chTable)
forM_ [a..b] $ \(!blkIdx) -> do
block <- loadBlockAt $! _chainLocation (_tableLongest chTable ! blkIdx)
-- when (mod blkIdx 500 == 0) $ print blkIdx -- hPrint stderr blkIdx
let txs = _blockTxs block
forM_ txs $ \(!tx) -> insertIntoTxLookup (_txHash tx) blkIdx txlkptable
return txlkptable
-- | Looks up a transaction by hash. First we check the 'TxLookup' table, then we load the possible blocks (if any),
-- parse them and check them for the given transaction. We also return the block height.
txLookup :: ChainTable -> TxLookup -> Hash256 -> IO (Maybe (Int, Tx RawScript RawScript))
txLookup chTable txTable hash = do
possible <- txLookupList hash txTable
results <- forM possible $ \blkIdx -> do
let loc = _chainLocation (_tableLongest chTable ! blkIdx)
block <- loadBlockAt loc
let txs = _blockTxs block
return $ case find (\tx -> hash == _txHash tx) txs of
Nothing -> Nothing
Just tx -> Just (blkIdx,tx)
case catMaybes results of
[] -> return $ Nothing
[!tx] -> return $ Just tx
_ -> error "txLookup: fatal error, multiple transactions found in with the same hash"
-- | does not return the block height
txLookup_ :: ChainTable -> TxLookup -> Hash256 -> IO (Maybe (Tx RawScript RawScript))
txLookup_ cht txt = liftM (liftM snd) . txLookup cht txt
--------------------------------------------------------------------------------
-- | Given a transaction, we load all the previous transactions from the disk (using the cache),
-- and add them the Tx structure (abusing the parametrized script field). The result can
-- then be fed to 'checkTransaction'.
--
loadPrevTxs :: ChainTable -> TxLookup -> Tx a b -> IO (Tx (Tx RawScript RawScript, a) b)
loadPrevTxs chTable txTable oldTx =
do
newInputs <- mapM worker (_txInputs oldTx)
return $! oldTx { _txInputs = newInputs }
where
lkp = txLookup_ chTable txTable
worker inp@(TxInput prevhash previdx script seqno) = do
mbprevtx <- lkp prevhash
let !prevtx = case mbprevtx of
Just tx -> tx
Nothing -> error $ "loadPrevTxs: fatal error: previous tx not found; hash = " ++ show prevhash
return $! inp { _txInScript = (prevtx,script) }
--------------------------------------------------------------------------------
-- | Checks a transaction. Note: this automatically accepts coinbase transactions
-- (does not check that that sum of reward and fees is the amount, since it has no access to the fees)
--
checkTx :: ChainTable -> TxLookup -> Tx RawScript RawScript -> IO (Either String Bool)
checkTx chTable txTable tx =
case (isCoinBaseTx tx) of
True -> return (Right True)
False -> do
txExt <- loadPrevTxs chTable txTable tx
return $ checkTransaction txExt
-- | Checks a transaction given by its hash
checkTxByHash :: ChainTable -> TxLookup -> Hash256 -> IO (Either String Bool)
checkTxByHash chTable txTable txid = do
mbtx <- txLookup_ chTable txTable txid
case mbtx of
Nothing -> error $ "checkTxByHash: tx not found (hash = " ++ show txid ++ ")"
Just tx -> checkTx chTable txTable tx
--------------------------------------------------------------------------------
{-
testCompactList = and
[ and [ (fromCompactList $ toCompactList [1..n] ) == [1..n] | n<-[0..77] ]
, and [ (fromCompactList $ consCompactList 666 (toCompactList [1..n])) == 666:[1..n] | n<-[0..77] ]
]
main = do
forM_ [0..(10::Int)] $ \n -> do
let xs = [(1::Int)..n]
il = toIndexList xs :: IndexList
cl = toCompactList xs :: CompactList
b1 = xs == fromIndexList il
b2 = xs == fromCompactList cl
k1 <- recursiveSize $!! xs
k2 <- recursiveSize $!! il
k3 <- recursiveSize $!! cl
print ( (length xs , k1,k2,k3, b1 , b2) :: (Int,Int,Int,Int,Bool,Bool) )
-}