Isolate Haskoin Core

This commit is contained in:
Jean-Pierre Rupp 2018-08-09 15:50:29 +01:00
parent 3279f5c830
commit 64b75c9f1c
109 changed files with 0 additions and 11777 deletions

View File

@ -1,164 +0,0 @@
# stylish-haskell configuration file
# ==================================
# The stylish-haskell tool is mainly configured by specifying steps. These steps
# are a list, so they have an order, and one specific step may appear more than
# once (if needed). Each file is processed by these steps in the given order.
steps:
# Convert some ASCII sequences to their Unicode equivalents. This is disabled
# by default.
# - unicode_syntax:
# # In order to make this work, we also need to insert the UnicodeSyntax
# # language pragma. If this flag is set to true, we insert it when it's
# # not already present. You may want to disable it if you configure
# # language extensions using some other method than pragmas. Default:
# # true.
# add_language_pragma: true
# Align the right hand side of some elements. This is quite conservative
# and only applies to statements where each element occupies a single
# line.
- simple_align:
cases: true
top_level_patterns: true
records: true
# Import cleanup
- imports:
# There are different ways we can align names and lists.
#
# - global: Align the import names and import list throughout the entire
# file.
#
# - file: Like global, but don't add padding when there are no qualified
# imports in the file.
#
# - group: Only align the imports per group (a group is formed by adjacent
# import lines).
#
# - none: Do not perform any alignment.
#
# Default: global.
align: global
# Folowing options affect only import list alignment.
#
# List align has following options:
#
# - after_alias: Import list is aligned with end of import including
# 'as' and 'hiding' keywords.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - with_alias: Import list is aligned with start of alias or hiding.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - new_line: Import list starts always on new line.
#
# > import qualified Data.List as List
# > (concat, foldl, foldr, head, init, last, length)
#
# Default: after alias
list_align: after_alias
# Long list align style takes effect when import is too long. This is
# determined by 'columns' setting.
#
# - inline: This option will put as much specs on same line as possible.
#
# - new_line: Import list will start on new line.
#
# - new_line_multiline: Import list will start on new line when it's
# short enough to fit to single line. Otherwise it'll be multiline.
#
# - multiline: One line per import list entry.
# Type with contructor list acts like single import.
#
# > import qualified Data.Map as M
# > ( empty
# > , singleton
# > , ...
# > , delete
# > )
#
# Default: inline
long_list_align: inline
# List padding determines indentation of import list on lines after import.
# This option affects 'list_align' and 'long_list_align'.
list_padding: 4
# Separate lists option affects formating of import list for type
# or class. The only difference is single space between type and list
# of constructors, selectors and class functions.
#
# - true: There is single space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable (fold, foldl, foldMap))
#
# - false: There is no space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable(fold, foldl, foldMap))
#
# Default: true
separate_lists: true
# Language pragmas
- language_pragmas:
# We can generate different styles of language pragma lists.
#
# - vertical: Vertical-spaced language pragmas, one per line.
#
# - compact: A more compact style.
#
# - compact_line: Similar to compact, but wrap each line with
# `{-#LANGUAGE #-}'.
#
# Default: vertical.
style: vertical
# Align affects alignment of closing pragma brackets.
#
# - true: Brackets are aligned in same collumn.
#
# - false: Brackets are not aligned together. There is only one space
# between actual import and closing bracket.
#
# Default: true
align: true
# stylish-haskell can detect redundancy of some language pragmas. If this
# is set to true, it will remove those redundant pragmas. Default: true.
remove_redundant: true
# Replace tabs by spaces. This is disabled by default.
# - tabs:
# # Number of spaces to use for each tab. Default: 8, as specified by the
# # Haskell report.
# spaces: 8
# Remove trailing whitespace
- trailing_whitespace: {}
# A common setting is the number of columns (parts of) code will be wrapped
# to. Different steps take this into account. Default: 80.
columns: 80
# Sometimes, language extensions are specified in a cabal file or from the
# command line instead of using language pragmas in the file. stylish-haskell
# needs to be aware of these, so it can parse the file correctly.
#
# No language extensions are enabled by default.
language_extensions:
- DeriveDataTypeable
- EmptyDataDecls
- FlexibleContexts
- FlexibleInstances
- GADTs
- OverloadedStrings
- RecordWildCards

View File

@ -1,2 +0,0 @@
import Distribution.Simple
main = defaultMain

View File

@ -1,24 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@ -1,111 +0,0 @@
name: haskoin-node
version: 0.4.3
synopsis:
Implementation of a Bitoin node.
description:
haskoin-node provides an implementation of the Bitcoin network protocol
that allows you to synchronize headers (with SPV validation) and download
merkle blocks and full blocks. This package can be used to implement
wallets or other Bitcoin components that require talking to the Bitcoin
network. It provides the following features:
.
* Implementation of the Bitcoin network protocol
* Headertree implementation with SPV verification
* Headers-first synchronization
* Merkle block download from peers with bloom filters
* Full block download from peers
.
A wallet implementation using this package is available in haskoin-wallet.
homepage: http://github.com/haskoin/haskoin
bug-reports: http://github.com/haskoin/haskoin/issues
tested-with: GHC==8.0.2
stability: stable
license: PublicDomain
license-file: UNLICENSE
author: Philippe Laprade, Jean-Pierre Rupp
maintainer: xenog@protonmail.com
category: Bitcoin, Finance, Network
build-type: Simple
cabal-version: >= 1.9.2
source-repository head
type: git
location: git://github.com/haskoin/haskoin.git
library
hs-source-dirs: src
exposed-modules: Network.Haskoin.Node.HeaderTree
Network.Haskoin.Node.Checkpoints
Network.Haskoin.Node.Peer
Network.Haskoin.Node.BlockChain
Network.Haskoin.Node.STM
other-modules: Network.Haskoin.Node.HeaderTree.Types
Network.Haskoin.Node.HeaderTree.Model
extensions: OverloadedStrings
FlexibleInstances
FlexibleContexts
RecordWildCards
DeriveDataTypeable
build-depends: aeson >= 1.2 && < 1.3
, async >= 2.0 && < 2.2
, base >= 4.8 && < 5
, bytestring >= 0.10 && < 0.11
, concurrent-extra >= 0.7 && < 0.8
, cereal >= 0.5 && < 0.6
, conduit >= 1.2 && < 1.3
, conduit-extra >= 1.2 && < 1.3
, containers >= 0.5 && < 0.6
, data-default >= 0.5 && < 0.8
, deepseq >= 1.4 && < 1.5
, either >= 4.5 && < 4.6
, esqueleto >= 2.4 && < 2.6
, exceptions >= 0.8 && < 0.9
, haskoin-core >= 0.3 && < 0.5
, largeword >= 1.2.4 && < 1.3
, lifted-async >= 0.2 && < 0.10
, lifted-base >= 0.2 && < 0.3
, monad-control >= 1.0 && < 1.1
, monad-logger >= 0.3 && < 0.4
, mtl >= 2.2 && < 2.3
, network >= 2.4 && < 2.7
, persistent >= 2.7 && < 2.8
, persistent-template >= 2.5 && < 2.6
, resource-pool >= 0.2 && < 0.3
, random >= 1.0 && < 1.2
, stm >= 2.4 && < 2.5
, stm-chans >= 3.0 && < 3.1
, stm-conduit >= 2.5 && < 3.1
, string-conversions >= 0.4 && < 0.5
, text >= 0.11 && < 1.3
, time >= 1.8 && < 1.9
ghc-options: -Wall
test-suite test-haskoin-node
type: exitcode-stdio-1.0
main-is: Main.hs
extensions: EmptyDataDecls
other-modules: Network.Haskoin.Node.Tests
Network.Haskoin.Node.Units
build-depends: base >= 4.8 && < 5
, haskoin-core
, haskoin-node
, HUnit >= 1.6 && < 1.7
, QuickCheck >= 2.10 && < 2.11
, monad-logger >= 0.3 && < 0.4
, mtl >= 2.2 && < 2.3
, persistent >= 2.7 && < 2.8
, persistent-sqlite >= 2.6 && < 2.7
, resourcet >= 1.1 && < 1.2
, test-framework >= 0.8 && < 0.9
, test-framework-quickcheck2 >= 0.3 && < 0.4
, test-framework-hunit >= 0.3 && < 0.4
ghc-options: -Wall
hs-source-dirs: test

View File

@ -1,713 +0,0 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
module Network.Haskoin.Node.BlockChain where
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async.Lifted (link, mapConcurrently,
waitAnyCancel, withAsync)
import Control.Concurrent.STM (STM, atomically, isEmptyTMVar,
putTMVar, readTVar, retry,
takeTMVar, tryReadTMVar,
tryTakeTMVar)
import Control.Concurrent.STM.Lock (locked)
import qualified Control.Concurrent.STM.Lock as Lock (with)
import Control.Concurrent.STM.TBMChan (isEmptyTBMChan, readTBMChan)
import Control.Exception.Lifted (throw)
import Control.Monad (forM, forM_, forever, unless,
void, when)
import Control.Monad.Logger (MonadLoggerIO, logDebug,
logError, logInfo, logWarn)
import Control.Monad.Reader (ask, asks)
import Control.Monad.Trans (MonadIO, lift, liftIO)
import Control.Monad.Trans.Control (MonadBaseControl, liftBaseOp_)
import qualified Data.ByteString.Char8 as C (unpack)
import Data.Conduit (Source, yield)
import Data.List (nub)
import qualified Data.Map as M (delete, keys, lookup,
null)
import Data.Maybe (listToMaybe)
import qualified Data.Sequence as S (length)
import Data.String.Conversions (cs)
import Data.Text (pack)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Data.Unique (hashUnique)
import Data.Word (Word32)
import Network.Haskoin.Block
import Network.Haskoin.Network
import Network.Haskoin.Node.HeaderTree
import Network.Haskoin.Node.Peer
import Network.Haskoin.Node.STM
import Network.Haskoin.Transaction
import System.Random (randomIO)
startSPVNode :: (MonadLoggerIO m, MonadBaseControl IO m)
=> [PeerHost]
-> BloomFilter
-> Int
-> NodeT m ()
startSPVNode hosts bloom elems = do
$(logDebug) "Setting our bloom filter in the node"
atomicallyNodeT $ sendBloomFilter bloom elems
$(logDebug) $ pack $ unwords
[ "Starting SPV node with", show $ length hosts, "hosts" ]
withAsync (void $ mapConcurrently startReconnectPeer hosts) $ \a1 -> do
link a1
$(logInfo) "Starting the initial header sync"
headerSync
$(logInfo) "Initial header sync complete"
$(logDebug) "Starting the tickle processing thread"
withAsync processTickles $ \a2 -> link a2 >> do
_ <- liftIO $ waitAnyCancel [a1, a2]
return ()
$(logDebug) "Exiting SPV-node thread"
-- Source of all transaction broadcasts
txSource :: (MonadLoggerIO m, MonadBaseControl IO m)
=> Source (NodeT m) Tx
txSource = do
chan <- lift $ asks sharedTxChan
$(logDebug) "Waiting to receive a transaction..."
resM <- liftIO $ atomically $ readTBMChan chan
case resM of
Just (pid, ph, tx) -> do
$(logInfo) $ formatPid pid ph $ unwords
[ "Received transaction broadcast", cs $ txHashToHex $ txHash tx ]
yield tx >> txSource
_ -> $(logError) "Tx channel closed unexpectedly"
handleGetData :: (MonadLoggerIO m, MonadBaseControl IO m)
=> (TxHash -> m (Maybe Tx))
-> NodeT m ()
handleGetData handler = forever $ do
$(logDebug) "Waiting for GetData transaction requests..."
-- Wait for tx GetData requests to be available
txids <- atomicallyNodeT $ do
datMap <- readTVarS sharedTxGetData
if M.null datMap then lift retry else return $ M.keys datMap
forM (nub txids) $ \tid -> lift (handler tid) >>= \txM -> do
$(logDebug) $ pack $ unwords
[ "Processing GetData txid request", cs $ txHashToHex tid ]
pidsM <- atomicallyNodeT $ do
datMap <- readTVarS sharedTxGetData
writeTVarS sharedTxGetData $ M.delete tid datMap
return $ M.lookup tid datMap
case (txM, pidsM) of
-- Send the transaction to the required peers
(Just tx, Just pids) -> forM_ pids $ \(pid, ph) -> do
$(logDebug) $ formatPid pid ph $ unwords
[ "Sending tx", cs $ txHashToHex tid, "to peer" ]
atomicallyNodeT $ trySendMessage pid $ MTx tx
_ -> return ()
broadcastTxs :: (MonadLoggerIO m, MonadBaseControl IO m)
=> [TxHash]
-> NodeT m ()
broadcastTxs txids = do
forM_ txids $ \tid -> $(logInfo) $ pack $ unwords
[ "Transaction INV broadcast:", cs $ txHashToHex tid ]
-- Broadcast an INV message for new transactions
let msg = MInv $ Inv $ map (InvVector InvTx . getTxHash) txids
atomicallyNodeT $ sendMessageAll msg
rescanTs :: Timestamp -> NodeT STM ()
rescanTs ts = do
rescanTMVar <- asks sharedRescan
lift $ do
-- Make sure the TMVar is empty
_ <- tryTakeTMVar rescanTMVar
putTMVar rescanTMVar $ Left ts
rescanHeight :: BlockHeight -> NodeT STM ()
rescanHeight h = do
rescanTMVar <- asks sharedRescan
lift $ do
-- Make sure the TMVar is empty
_ <- tryTakeTMVar rescanTMVar
putTMVar rescanTMVar $ Right h
-- Wait for the next merkle batch to be available. This function will check for
-- rescans.
merkleDownload
:: (MonadLoggerIO m, MonadBaseControl IO m)
=> BlockHash
-> Word32
-> NodeT m
(BlockChainAction, Source (NodeT m) (Either (MerkleBlock, MerkleTxs) Tx))
merkleDownload walletHash batchSize = do
-- Store the best block received from the wallet for information only.
-- This will be displayed in `hw status`
merkleSyncedActions walletHash
walletBlockM <- runSqlNodeT $ getBlockByHash walletHash
walletBlock <- case walletBlockM of
Just walletBlock -> do
atomicallyNodeT $ writeTVarS sharedBestBlock walletBlock
return walletBlock
Nothing ->
error "Could not find wallet best block in headers"
rescanTMVar <- asks sharedRescan
-- Wait either for a new block to arrive or a rescan to be triggered
$(logDebug) "Waiting for a new block or a rescan..."
resE <- atomicallyNodeT $ orElseNodeT
(fmap Left $ lift $ takeTMVar rescanTMVar)
(const (Right ()) <$> waitNewBlock walletHash)
resM <- case resE of
-- A rescan was triggered
Left valE -> do
$(logInfo) $ pack $ unwords
[ "Got rescan request", show valE ]
-- Wait until rescan conditions are met
newValE <- waitRescan rescanTMVar valE
$(logDebug) $ pack $ unwords
[ "Rescan condition reached:", show newValE ]
case newValE of
Left ts -> tryMerkleDwnTimestamp ts batchSize
Right _ -> tryMerkleDwnHeight walletBlock batchSize
-- Continue download from a hash
Right _ -> tryMerkleDwnBlock walletBlock batchSize
case resM of
Just res -> return res
_ -> do
$(logWarn) "Invalid merkleDownload result. Retrying ..."
-- Sleep 10 seconds and retry
liftIO $ threadDelay $ 10*1000000
merkleDownload walletHash batchSize
where
waitRescan rescanTMVar valE = do
resE <- atomicallyNodeT $ orElseNodeT
(fmap Left (lift $ takeTMVar rescanTMVar))
(waitVal valE >> return (Right valE))
case resE of
Left newValE -> waitRescan rescanTMVar newValE
Right res -> return res
waitVal valE = case valE of
Left ts -> waitFastCatchup ts
Right h -> waitHeight h
-- | Perform some actions only when headers have been synced.
merkleSyncedActions
:: (MonadLoggerIO m, MonadBaseControl IO m)
=> BlockHash -- ^ Wallet best block
-> NodeT m ()
merkleSyncedActions walletHash =
asks sharedSyncLock >>= \lock -> liftBaseOp_ (Lock.with lock) $ do
-- Check if we are synced
(synced, mempool, header) <- atomicallyNodeT $ do
header <- readTVarS sharedBestHeader
synced <- areBlocksSynced walletHash
mempool <- readTVarS sharedMempool
return (synced, mempool, header)
when synced $ do
$(logInfo) $ pack $ unwords
[ "Merkle blocks are in sync with the"
, "network at height", show walletHash
]
-- Prune side chains
bestBlock <- runSqlNodeT $ pruneChain header
atomicallyNodeT $ do
-- Update shared best header after pruning
writeTVarS sharedBestHeader bestBlock
writeTVarS sharedMerklePeer Nothing
-- Do a mempool sync on the first merkle sync
unless mempool $ do
atomicallyNodeT $ do
sendMessageAll MMempool
writeTVarS sharedMempool True
$(logInfo) "Requesting a mempool sync"
-- Wait for headers to catch up to the given height
waitHeight :: BlockHeight -> NodeT STM ()
waitHeight height = do
node <- readTVarS sharedBestHeader
-- Check if we passed the timestamp condition
unless (height < nodeBlockHeight node) $ lift retry
-- Wait for headers to catch up to the given timestamp
waitFastCatchup :: Timestamp -> NodeT STM ()
waitFastCatchup ts = do
node <- readTVarS sharedBestHeader
-- Check if we passed the timestamp condition
unless (ts < blockTimestamp (nHeader node)) $
lift retry
-- Wait for a new block to be available for download
waitNewBlock :: BlockHash -> NodeT STM ()
waitNewBlock bh = do
node <- readTVarS sharedBestHeader
-- We have more merkle blocks to download
unless (bh /= nodeHash node) $
lift retry
tryMerkleDwnHeight
:: (MonadLoggerIO m, MonadBaseControl IO m)
=> NodeBlock
-> Word32
-> NodeT m (Maybe (BlockChainAction,
Source (NodeT m) (Either (MerkleBlock, MerkleTxs) Tx)))
tryMerkleDwnHeight block batchSize = do
$(logInfo) $ pack $ unwords
[ "Requesting merkle blocks at height", show $ nodeBlockHeight block
, "with batch size", show batchSize
]
-- Request height - 1 as we want to start downloading at height
nodeM <- runSqlNodeT $ getParentBlock block
case nodeM of
Just bn ->
tryMerkleDwnBlock bn batchSize
_ -> do
$(logDebug) $ pack $ unwords
[ "Can't download merkle blocks."
, "Waiting for headers to sync ..."
]
return Nothing
tryMerkleDwnTimestamp
:: (MonadLoggerIO m, MonadBaseControl IO m)
=> Timestamp
-> Word32
-> NodeT m (Maybe (BlockChainAction,
Source (NodeT m) (Either (MerkleBlock, MerkleTxs) Tx)))
tryMerkleDwnTimestamp ts batchSize = do
$(logInfo) $ pack $ unwords
[ "Requesting merkle blocks after timestamp", show ts
, "with batch size", show batchSize
]
nodeM <- runSqlNodeT $ getBlockAfterTime ts
case nodeM of
Just bh ->
tryMerkleDwnBlock bh batchSize
_ -> do
$(logDebug) $ pack $ unwords
[ "Can't download merkle blocks."
, "Waiting for headers to sync ..."
]
return Nothing
tryMerkleDwnBlock
:: (MonadLoggerIO m, MonadBaseControl IO m)
=> NodeBlock
-> Word32
-> NodeT m (Maybe (BlockChainAction,
Source (NodeT m) (Either (MerkleBlock, MerkleTxs) Tx)))
tryMerkleDwnBlock bh batchSize = do
$(logDebug) $ pack $ unwords
[ "Requesting merkle download from block"
, cs $ blockHashToHex (nodeHash bh)
, "and batch size", show batchSize
]
-- Get the list of merkle blocks to download from our headers
best <- atomicallyNodeT $ readTVarS sharedBestHeader
action <- runSqlNodeT $ getBlockWindow best bh batchSize
case actionNodes action of
[] -> do
$(logError) "BlockChainAction was empty"
return Nothing
ns -> do
-- Wait for a peer available for merkle download
(pid, PeerSession{..}) <- waitMerklePeer $
nodeBlockHeight $ last ns
$(logDebug) $ formatPid pid peerSessionHost $ unwords
[ "Found merkle downloading peer with score"
, show peerSessionScore
]
let source = peerMerkleDownload pid peerSessionHost action
return $ Just (action, source)
where
waitMerklePeer height = atomicallyNodeT $ do
pidM <- readTVarS sharedHeaderPeer
allPeers <- getPeersAtHeight (>= height)
let f (pid,_) = Just pid /= pidM
-- Filter out the peer syncing headers (if there is one)
peers = filter f allPeers
case listToMaybe peers of
Just res@(pid,_) -> do
writeTVarS sharedMerklePeer $ Just pid
return res
_ -> lift retry
peerMerkleDownload
:: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerId
-> PeerHost
-> BlockChainAction
-> Source (NodeT m) (Either (MerkleBlock, MerkleTxs) Tx)
peerMerkleDownload pid ph action = do
let bids = map nodeHash $ actionNodes action
vs = map (InvVector InvMerkleBlock . getBlockHash) bids
$(logInfo) $ formatPid pid ph $ unwords
[ "Requesting", show $ length bids, "merkle block(s)" ]
nonce <- liftIO randomIO
-- Request a merkle batch download
sessM <- lift . atomicallyNodeT $ do
_ <- trySendMessage pid $ MGetData $ GetData vs
-- Send a ping to have a recognizable end message for
-- the last merkle block download.
_ <- trySendMessage pid $ MPing $ Ping nonce
tryGetPeerSession pid
case sessM of
Just PeerSession{..} -> checkOrder peerSessionMerkleChan bids
_ -> lift . atomicallyNodeT $
writeTVarS sharedMerklePeer Nothing
where
-- Build a source that that will check the order of the received merkle
-- blocks against the initial request. If merkle blocks are sent out of
-- order, the source will close and the peer will be flagged as
-- misbehaving. The source will also close once all the requested merkle
-- blocks have been received from the peer.
checkOrder _ [] = lift . atomicallyNodeT $
writeTVarS sharedMerklePeer Nothing
checkOrder chan (bid:bids) = do
-- Read the channel or disconnect the peer after waiting for 2 minutes
resM <- lift $ raceTimeout 120
(disconnectPeer pid ph)
(liftIO . atomically $ readTBMChan chan)
case resM of
-- Forward transactions
Right (Just res@(Right _)) ->
yield res >> checkOrder chan (bid:bids)
Right (Just res@(Left (MerkleBlock mHead _ _ _, _))) -> do
let mBid = headerHash mHead
$(logDebug) $ formatPid pid ph $ unwords
[ "Processing merkle block", cs $ blockHashToHex mBid ]
-- Check if we were expecting this merkle block
if mBid == bid
then yield res >> checkOrder chan bids
else lift $ do
atomicallyNodeT $ writeTVarS sharedMerklePeer Nothing
-- If we were not expecting this merkle block, do not
-- yield the merkle block and close the source
misbehaving pid ph moderateDoS $ unwords
[ "Peer sent us merkle block hash"
, cs $ blockHashToHex $ headerHash mHead
, "but we expected merkle block hash"
, cs $ blockHashToHex bid
]
-- Not sure how to recover from this situation.
-- Disconnect the peer. TODO: Is there a way to recover
-- without buffering the whole batch in memory and
-- re-order it?
disconnectPeer pid ph
-- The channel closed. Stop here.
_ -> do
$(logWarn) $ formatPid pid ph
"Merkle channel closed unexpectedly"
lift $ atomicallyNodeT $ writeTVarS sharedMerklePeer Nothing
processTickles :: (MonadLoggerIO m, MonadBaseControl IO m)
=> NodeT m ()
processTickles = forever $ do
$(logDebug) $ pack "Waiting for a block tickle ..."
(pid, ph, tickle) <- atomicallyNodeT waitTickle
$(logInfo) $ formatPid pid ph $ unwords
[ "Received block tickle", cs $ blockHashToHex tickle ]
heightM <- fmap nodeBlockHeight <$> runSqlNodeT (getBlockByHash tickle)
case heightM of
Just height -> do
$(logInfo) $ formatPid pid ph $ unwords
[ "The block tickle", cs $ blockHashToHex tickle
, "is already connected"
]
updatePeerHeight pid ph height
_ -> do
$(logDebug) $ formatPid pid ph $ unwords
[ "The tickle", cs $ blockHashToHex tickle
, "is unknown. Requesting a peer header sync."
]
peerHeaderSyncFull pid ph `catchAny` const (disconnectPeer pid ph)
newHeightM <-
fmap nodeBlockHeight <$> runSqlNodeT (getBlockByHash tickle)
case newHeightM of
Just height -> do
$(logInfo) $ formatPid pid ph $ unwords
[ "The block tickle", cs $ blockHashToHex tickle
, "was connected successfully"
]
updatePeerHeight pid ph height
_ -> $(logWarn) $ formatPid pid ph $ unwords
[ "Could not find the height of block tickle"
, cs $ blockHashToHex tickle
]
where
updatePeerHeight pid ph height = do
$(logInfo) $ formatPid pid ph $ unwords
[ "Updating peer height to", show height ]
atomicallyNodeT $ do
modifyPeerSession pid $ \s ->
s{ peerSessionHeight = height }
updateNetworkHeight
waitTickle :: NodeT STM (PeerId, PeerHost, BlockHash)
waitTickle = do
tickleChan <- asks sharedTickleChan
resM <- lift $ readTBMChan tickleChan
case resM of
Just res -> return res
_ -> throw $ NodeException "tickle channel closed unexpectedly"
syncedHeight :: MonadIO m => NodeT m (Bool, Word32)
syncedHeight = atomicallyNodeT $ do
synced <- areHeadersSynced
ourHeight <- nodeBlockHeight <$> readTVarS sharedBestHeader
return (synced, ourHeight)
headerSync :: (MonadLoggerIO m, MonadBaseControl IO m)
=> NodeT m ()
headerSync = do
-- Start the header sync
$(logDebug) "Syncing more headers. Finding the best peer..."
(pid, PeerSession{..}) <- atomicallyNodeT $ do
peers <- getPeersAtNetHeight
case listToMaybe peers of
Just res@(pid,_) -> do
-- Save the header syncing peer
writeTVarS sharedHeaderPeer $ Just pid
return res
_ -> lift retry
$(logDebug) $ formatPid pid peerSessionHost $ unwords
[ "Found best header syncing peer with score"
, show peerSessionScore
]
-- Run a maximum of 10 header downloads with this peer.
-- Then we re-evaluate the best peer
continue <- catchAny (peerHeaderSyncLimit pid peerSessionHost 10) $
\e -> do
$(logError) $ pack $ unwords ["peerHeaderSyncLimit:", show e]
disconnectPeer pid peerSessionHost >> return True
-- Reset the header syncing peer
atomicallyNodeT $ writeTVarS sharedHeaderPeer Nothing
-- Check if we should continue the header sync
if continue then headerSync else do
(synced, ourHeight) <- syncedHeight
if synced
then
$(logInfo) $ formatPid pid peerSessionHost $ unwords
[ "Block headers are in sync with the"
, "network at height", show ourHeight
]
-- Continue the download if we are not yet synced
else headerSync
peerHeaderSyncLimit :: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerId
-> PeerHost
-> Int
-> NodeT m Bool
peerHeaderSyncLimit pid ph initLimit
| initLimit < 1 = error "Limit must be at least 1"
| otherwise = go initLimit Nothing
where
go limit prevM = peerHeaderSync pid ph prevM >>= \actionM -> case actionM of
Just action ->
-- If we received a side chain or a known chain, we want to
-- continue szncing from this peer even if the limit has been
-- reached.
if limit > 1 || isSideChain action || isKnownChain action
then go (limit - 1) actionM
-- We got a Just, so we can continue the download from
-- this peer
else return True
_ -> return False
-- Sync all the headers from a given peer
peerHeaderSyncFull :: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerId
-> PeerHost
-> NodeT m ()
peerHeaderSyncFull pid ph =
go Nothing
where
go prevM = peerHeaderSync pid ph prevM >>= \actionM -> case actionM of
Just _ -> go actionM
Nothing -> do
(synced, ourHeight) <- syncedHeight
when synced $ $(logInfo) $ formatPid pid ph $ unwords
[ "Block headers are in sync with the"
, "network at height", show ourHeight
]
areBlocksSynced :: BlockHash -> NodeT STM Bool
areBlocksSynced walletHash = do
headersSynced <- areHeadersSynced
bestHeader <- readTVarS sharedBestHeader
return $ headersSynced && walletHash == nodeHash bestHeader
-- Check if the block headers are synced with the network height
areHeadersSynced :: NodeT STM Bool
areHeadersSynced = do
ourHeight <- nodeBlockHeight <$> readTVarS sharedBestHeader
netHeight <- readTVarS sharedNetworkHeight
-- If netHeight == 0 then we did not connect to any peers yet
return $ ourHeight >= netHeight && netHeight > 0
-- | Sync one batch of headers from the given peer. Accept the result of a
-- previous peerHeaderSync to correctly compute block locators in the
-- presence of side chains.
peerHeaderSync :: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerId
-> PeerHost
-> Maybe BlockChainAction
-> NodeT m (Maybe BlockChainAction)
peerHeaderSync pid ph prevM = do
$(logDebug) $ formatPid pid ph "Waiting for the HeaderSync lock"
-- Aquire the header syncing lock
lock <- asks sharedSyncLock
liftBaseOp_ (Lock.with lock) $ do
best <- atomicallyNodeT $ readTVarS sharedBestHeader
-- Retrieve the block locator
loc <- case prevM of
Just (KnownChain ns) -> do
$(logDebug) $ formatPid pid ph "Building a known chain locator"
runSqlNodeT $ bLocator $ last ns
Just (SideChain ns) -> do
$(logDebug) $ formatPid pid ph "Building a side chain locator"
runSqlNodeT $ bLocator $ last ns
Just (BestChain ns) -> do
$(logDebug) $ formatPid pid ph "Building a best chain locator"
runSqlNodeT $ bLocator $ last ns
Just (ChainReorg _ _ ns) -> do
$(logDebug) $ formatPid pid ph "Building a reorg locator"
runSqlNodeT $ bLocator $ last ns
Nothing -> do
$(logDebug) $ formatPid pid ph "Building a locator to best"
runSqlNodeT $ bLocator best
$(logDebug) $ formatPid pid ph $ unwords
[ "Requesting headers with block locator of size"
, show $ length loc
, "Start block:", cs $ blockHashToHex $ head loc
, "End block:", cs $ blockHashToHex $ last loc
]
-- Send a GetHeaders message to the peer
atomicallyNodeT $ sendMessage pid $ MGetHeaders $ GetHeaders 0x01 loc z
$(logDebug) $ formatPid pid ph "Waiting 2 minutes for headers..."
-- Wait 120 seconds for a response or time out
continueE <- raceTimeout 120 (disconnectPeer pid ph) (waitHeaders best)
-- Return True if we can continue syncing from this peer
return $ either (const Nothing) id continueE
where
z = "0000000000000000000000000000000000000000000000000000000000000000"
-- Wait for the headers to be available
waitHeaders best = do
(rPid, headers) <- atomicallyNodeT $ takeTMVarS sharedHeaders
if rPid == pid
then processHeaders best headers
else waitHeaders best
processHeaders _ (Headers []) = do
$(logDebug) $ formatPid pid ph
"Received empty headers. Finished downloading headers."
-- Do not continue the header download
return Nothing
processHeaders best (Headers hs) = do
$(logDebug) $ formatPid pid ph $ unwords
[ "Received", show $ length hs, "headers."
, "Start blocks:", cs $ blockHashToHex $ headerHash $ fst $ head hs
, "End blocks:", cs $ blockHashToHex $ headerHash $ fst $ last hs
]
now <- round <$> liftIO getPOSIXTime
actionE <- runSqlNodeT $ connectHeaders best (map fst hs) now
case actionE of
Left err -> do
misbehaving pid ph severeDoS err
return Nothing
Right action -> case actionNodes action of
[] -> do
$(logWarn) $ formatPid pid ph $ unwords
[ "Received an empty blockchain action:", show action ]
return Nothing
nodes -> do
$(logDebug) $ formatPid pid ph $ unwords
[ "Received", show $ length nodes
, "nodes in the action"
]
let height = nodeBlockHeight $ last nodes
case action of
KnownChain _ ->
$(logInfo) $ formatPid pid ph $ unwords
[ "KnownChain headers received"
, "up to height", show height
]
SideChain _ ->
$(logInfo) $ formatPid pid ph $ unwords
[ "SideChain headers connected successfully"
, "up to height", show height
]
-- Headers extend our current best head
_ -> do
$(logInfo) $ formatPid pid ph $ unwords
[ "Best headers connected successfully"
, "up to height", show height
]
atomicallyNodeT $
writeTVarS sharedBestHeader $ last nodes
-- If we received less than 2000 headers, we are done
-- syncing from this peer and we return Nothing.
return $ if length hs < 2000
then Nothing
else Just action
nodeStatus :: NodeT STM NodeStatus
nodeStatus = do
nodeStatusPeers <- mapM peerStatus =<< getPeers
SharedNodeState{..} <- ask
lift $ do
best <- readTVar sharedBestBlock
header <- readTVar sharedBestHeader
let nodeStatusBestBlock = nodeHash best
nodeStatusBestBlockHeight = nodeBlockHeight best
nodeStatusBestHeader = nodeHash header
nodeStatusBestHeaderHeight = nodeBlockHeight header
nodeStatusNetworkHeight <-
readTVar sharedNetworkHeight
nodeStatusBloomSize <-
maybe 0 (S.length . bloomData . fst) <$> readTVar sharedBloomFilter
nodeStatusHeaderPeer <-
fmap hashUnique <$> readTVar sharedHeaderPeer
nodeStatusMerklePeer <-
fmap hashUnique <$> readTVar sharedMerklePeer
nodeStatusHaveHeaders <-
not <$> isEmptyTMVar sharedHeaders
nodeStatusHaveTickles <-
not <$> isEmptyTBMChan sharedTickleChan
nodeStatusHaveTxs <-
not <$> isEmptyTBMChan sharedTxChan
nodeStatusGetData <-
M.keys <$> readTVar sharedTxGetData
nodeStatusRescan <-
tryReadTMVar sharedRescan
nodeStatusMempool <-
readTVar sharedMempool
nodeStatusSyncLock <-
locked sharedSyncLock
return NodeStatus{..}
peerStatus :: (PeerId, PeerSession) -> NodeT STM PeerStatus
peerStatus (pid, PeerSession{..}) = do
hostM <- getHostSession peerSessionHost
let peerStatusPeerId = hashUnique pid
peerStatusHost = peerSessionHost
peerStatusConnected = peerSessionConnected
peerStatusHeight = peerSessionHeight
peerStatusProtocol = version <$> peerSessionVersion
peerStatusUserAgent =
C.unpack . getVarString . userAgent <$> peerSessionVersion
peerStatusPing = show <$> peerSessionScore
peerStatusDoSScore = peerHostSessionScore <$> hostM
peerStatusLog = peerHostSessionLog <$> hostM
peerStatusReconnectTimer = peerHostSessionReconnect <$> hostM
lift $ do
peerStatusHaveMerkles <- not <$> isEmptyTBMChan peerSessionMerkleChan
peerStatusHaveMessage <- not <$> isEmptyTBMChan peerSessionChan
peerStatusPingNonces <- readTVar peerSessionPings
return PeerStatus{..}

View File

@ -1,30 +0,0 @@
module Network.Haskoin.Node.Checkpoints
( checkpointMap
, checkpointList
, verifyCheckpoint
) where
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M (fromList, lookup)
import Data.Word (Word32)
import Network.Haskoin.Block
import Network.Haskoin.Constants
-- | Checkpoints from bitcoind reference implementation /src/checkpoints.cpp
-- presented as an IntMap.
checkpointMap :: Map Word32 BlockHash
checkpointMap = M.fromList checkpointList
-- | Checkpoints from bitcoind reference implementation /src/checkpoints.cpp
-- presented as a list.
checkpointList :: [(Word32, BlockHash)]
checkpointList = checkpoints
-- | Verify that a block hash at a given height either matches an existing
-- checkpoint or is not a checkpoint.
verifyCheckpoint :: Word32 -> BlockHash -> Bool
verifyCheckpoint height hash = case M.lookup height checkpointMap of
Just value -> hash == value
Nothing -> True

View File

@ -1,640 +0,0 @@
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
module Network.Haskoin.Node.HeaderTree
( BlockChainAction(..)
, BlockHeight
, NodeBlock
, Timestamp
, initHeaderTree
, migrateHeaderTree
, getBestBlock
, getHeads
, getBlockByHash
, getParentBlock
, getBlockWindow
, getBlockAfterTime
, getChildBlocks
, getBlockByHeight
, getBlocksByHeight
, getBlocksFromHeight
, getBlocksAtHeight
, putBlock
, putBlocks
, nGenesisBlock
, splitBlock
, splitChains
, nodeBlock
, nodeBlockHeight
, nodeHash
, nHeader
, nodePrev
, nodeTimestamp
, nWork
, nHeight
, nodeChain
, isBestChain
, isChainReorg
, isSideChain
, isKnownChain
, connectHeader
, connectHeaders
, bLocator
, pruneChain
) where
import Control.Monad (foldM, forM, unless,
when, (<=<))
import Control.Monad.Except (ExceptT (..),
runExceptT, throwError)
import Control.Monad.State (evalStateT, get, put)
import Control.Monad.Trans (MonadIO, lift)
import Data.Bits (shiftL)
import qualified Data.ByteString as BS (take)
import Data.Either (fromRight)
import Data.Function (on)
import Data.List (find, maximumBy, sort)
import Data.Maybe (fromMaybe, isNothing,
listToMaybe, mapMaybe)
import Data.Serialize (decode, encode)
import Data.String.Conversions (cs)
import Data.Word (Word32)
import Database.Esqueleto (Esqueleto, Value, asc,
delete, from, groupBy,
in_, insertMany_, limit,
max_, not_, orderBy,
select, set, unValue,
update, val, valList,
where_, (!=.), (&&.),
(<=.), (=.), (==.),
(>.), (>=.), (^.),
(||.))
import Database.Persist (Entity (..), insert_)
import Database.Persist.Sql (SqlPersistT)
import Network.Haskoin.Block
import Network.Haskoin.Constants
import Network.Haskoin.Node.Checkpoints
import Network.Haskoin.Node.HeaderTree.Model
import Network.Haskoin.Node.HeaderTree.Types
data BlockChainAction
= BestChain { actionNodes :: ![NodeBlock] }
| ChainReorg { actionSplitNode :: !NodeBlock
, actionOldNodes :: ![NodeBlock]
, actionNodes :: ![NodeBlock]
}
| SideChain { actionNodes :: ![NodeBlock] }
| KnownChain { actionNodes :: ![NodeBlock] }
deriving (Show, Eq)
shortHash :: BlockHash -> ShortHash
shortHash =
fromRight (error "Could not decdoe block hash") .
decode . BS.take 8 . encode . getBlockHash
nHeader :: NodeBlock -> BlockHeader
nHeader = getNodeHeader . nodeBlockHeader
nodeHash :: NodeBlock -> BlockHash
nodeHash = headerHash . nHeader
nodePrev :: NodeBlock -> BlockHash
nodePrev = prevBlock . nHeader
nodeTimestamp :: NodeBlock -> Timestamp
nodeTimestamp = blockTimestamp . nHeader
nWork :: NodeBlock -> Work
nWork = nodeBlockWork
nHeight :: NodeBlock -> BlockHeight
nHeight = nodeBlockHeight
nodeChain :: NodeBlock -> Word32
nodeChain = nodeBlockChain
-- | Number of blocks on average between difficulty cycles (2016 blocks).
diffInterval :: Word32
diffInterval = targetTimespan `div` targetSpacing
-- | Genesis block.
nGenesisBlock :: NodeBlock
nGenesisBlock = NodeBlock
{ nodeBlockHash = shortHash $ headerHash genesisHeader
, nodeBlockHeader = NodeHeader genesisHeader
, nodeBlockWork = 1.0
, nodeBlockHeight = 0
, nodeBlockChain = 0
}
-- | Initialize the block header chain by inserting the genesis block if it
-- doesn't already exist.
initHeaderTree :: MonadIO m => SqlPersistT m ()
initHeaderTree = do
nodeM <- getBlockByHash $ headerHash genesisHeader
when (isNothing nodeM) $ putBlock nGenesisBlock
getVerifyParams
:: MonadIO m
=> BlockHeader
-> ExceptT String (SqlPersistT m)
(NodeBlock, [Timestamp], Timestamp, Word32, Maybe Word32)
getVerifyParams bh = do
parentM <- lift $ getBlockByHash $ prevBlock bh
parent <- maybe (throwError "Could not get parent node") return parentM
checkPointM <- fmap nodeBlockHeight <$> lift lastSeenCheckpoint
diffBlockM <- lift $ getBlockByHeight parent $
nodeBlockHeight parent `div` diffInterval * diffInterval
diffTime <- maybe (throwError "Could not get difficulty change block")
(return . nodeTimestamp)
diffBlockM
medianBlocks <- lift $ map nodeTimestamp <$>
getBlocksFromHeight parent 11 (min 0 $ nodeBlockHeight parent - 10)
minWork <- lift $ findMinWork parent
return (parent, medianBlocks, diffTime, minWork, checkPointM)
findMinWork :: MonadIO m => NodeBlock -> SqlPersistT m MinWork
findMinWork bn
| isMinWork bn = return $ blockBits $ nHeader bn
| otherwise = getParentBlock bn >>=
maybe (return $ blockBits $ nHeader bn) findMinWork
isMinWork :: NodeBlock -> Bool
isMinWork bn
| not allowMinDifficultyBlocks = True
| nodeBlockHeight bn `mod` diffInterval == 0 = True
| blockBits (nHeader bn) /= encodeCompact powLimit = True
| otherwise = False
splitKnown :: MonadIO m
=> [BlockHeader]
-> SqlPersistT m ([NodeBlock], [BlockHeader])
splitKnown hs = do
(kno, unk) <- foldM f ([], []) hs
return (reverse kno, reverse unk)
where
f (kno, []) n = do
bnM <- getBlockByHash (headerHash n)
case bnM of
Nothing -> return (kno, [n])
Just bn -> return (bn:kno, [])
f (kno, unk) n = return (kno, n:unk)
-- | Connect a block header to this block header chain. Corresponds to bitcoind
-- function ProcessBlockHeader and AcceptBlockHeader in main.cpp.
connectHeader :: MonadIO m
=> NodeBlock
-> BlockHeader
-> Timestamp
-> SqlPersistT m (Either String BlockChainAction)
connectHeader best bh ts = runExceptT $ do
(kno, _) <- lift $ splitKnown [bh]
case kno of
[] -> do
(parent, medians, diffTime, minWork, cpM) <- getVerifyParams bh
chain <- lift $ getChain parent
let bn = nodeBlock parent chain bh
ExceptT . return $
verifyBlockHeader parent medians diffTime cpM minWork ts bh
lift $ putBlock bn
lift $ evalNewChain best [bn]
_ -> return $ KnownChain kno
-- | A more efficient way of connecting a list of block headers than connecting
-- them individually. The list of block headers have must form a valid chain.
connectHeaders :: MonadIO m
=> NodeBlock
-> [BlockHeader]
-> Timestamp
-> SqlPersistT m (Either String BlockChainAction)
connectHeaders _ [] _ = runExceptT $ throwError "Nothing to connect"
connectHeaders best bhs ts = runExceptT $ do
unless (validChain bhs) $ throwError "Block headers do not form a valid chain"
(kno, unk) <- lift $ splitKnown bhs
case unk of
[] -> return $ KnownChain kno
(bh:_) -> do
(parent, medians, diffTime, minWork, cpM) <- getVerifyParams bh
chain <- lift $ getChain parent
nodes <- (`evalStateT` (parent, diffTime, medians, minWork)) $
forM unk $ \b -> do
(p, d, ms, mw) <- get
lift . ExceptT . return $ verifyBlockHeader p ms d cpM mw ts b
let bn = nodeBlock p chain b
d' = if nodeBlockHeight bn `mod` diffInterval == 0
then blockTimestamp b
else d
ms' = blockTimestamp b : if length ms == 11
then tail ms
else ms
mw' = if isMinWork bn then blockBits b else mw
put (bn, d', ms', mw')
return bn
lift $ putBlocks nodes
lift $ evalNewChain best nodes
where
validChain (a:b:xs) = prevBlock b == headerHash a && validChain (b:xs)
validChain [_] = True
validChain _ = False
-- | 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
-- | Returns a BlockLocator object for a given block hash.
bLocator :: MonadIO m => NodeBlock -> SqlPersistT m BlockLocator
bLocator node = do
nodes <- getBlocksByHeight node bs
return $ map nodeHash nodes
where
h = nodeBlockHeight node
f x s = (fst x - s, fst x > s)
bs = (++ [0]) $ map fst $ takeWhile snd $
[(h - x, x < h) | x <- [0..9]] ++
scanl f (h - 10, h > 10) [2 ^ (x :: Word32) | x <- [1..]]
-- | Verify block header conforms to protocol.
verifyBlockHeader :: NodeBlock -- ^ Parent block header
-> [Timestamp] -- ^ Timestamps of previous 11 blocks
-> Timestamp -- ^ Previous difficulty change
-> Maybe Word32 -- ^ Height of most recent checkpoint
-> MinWork -- ^ Last MinWork (e.g. Testnet3)
-> Timestamp -- ^ Current time
-> BlockHeader -- ^ Block header to validate
-> Either String ()
-- TODO: Add DOS return values
verifyBlockHeader par mts dt cp mw ts bh = do
unless (isValidPOW bh) $
Left "Invalid proof of work"
unless (blockTimestamp bh <= ts + 2 * 60 * 60) $
Left "Invalid header timestamp"
let nextWork = nextWorkRequired par dt mw bh
unless (blockBits bh == nextWork) $
Left "Incorrect work transition (bits)"
let sortedMedians = sort mts
medianTime = sortedMedians !! (length sortedMedians `div` 2)
when (blockTimestamp bh <= medianTime) $
Left "Block timestamp is too early"
let newHeight = nodeBlockHeight par + 1
unless (maybe True (fromIntegral newHeight >) cp) $
Left "Rewriting pre-checkpoint chain"
unless (verifyCheckpoint (fromIntegral newHeight) (headerHash bh)) $
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
&& nodeBlockHeight par + 1 >= 227836) $
Left "Rejected version 1 block"
-- | Create a block node data structure from a block header.
nodeBlock :: NodeBlock -- ^ Parent block node
-> Word32 -- ^ Chain number for new node
-> BlockHeader
-> NodeBlock
nodeBlock parent chain bh = NodeBlock
{ nodeBlockHash = shortHash $ headerHash bh
, nodeBlockHeader = NodeHeader bh
, nodeBlockWork = newWork
, nodeBlockHeight = height
, nodeBlockChain = chain
}
where
newWork = nodeBlockWork parent + fromIntegral
(headerWork bh `div` headerWork genesisHeader)
height = nodeBlockHeight parent + 1
-- | Return blockchain action to connect given block with best block. Count will
-- limit the amount of blocks building up from split point towards the best
-- block.
getBlockWindow :: MonadIO m
=> NodeBlock -- ^ Best block
-> NodeBlock -- ^ Start of window
-> Word32 -- ^ Window count
-> SqlPersistT m BlockChainAction
getBlockWindow best node cnt = do
(_, old, new) <- splitChains (node, 0) (best, cnt)
return $ if null old then BestChain new else ChainReorg node old new
-- | Find the split point between two nodes. It also returns the two partial
-- chains leading from the split point to the respective nodes. Tuples must
-- contain a block node and the count of nodes that should be returned from the
-- split towards that block. 0 means all.
splitChains :: MonadIO m
=> (NodeBlock, Word32)
-> (NodeBlock, Word32)
-> SqlPersistT m (NodeBlock, [NodeBlock], [NodeBlock])
splitChains (l, ln) (r, rn) = do
sn <- splitBlock l r
(split:ls) <- getBlocksFromHeight l ln (nodeBlockHeight sn)
rs <- getBlocksFromHeight r rn (nodeBlockHeight sn + 1)
return (split, ls, rs)
-- | Finds the parent of a block.
getParentBlock :: MonadIO m
=> NodeBlock
-> SqlPersistT m (Maybe NodeBlock)
getParentBlock node
| nodeBlockHeight node == 0 = return Nothing
| otherwise = getBlockByHash p
where
p = nodePrev node
-- | Get all children for a block
getChildBlocks :: MonadIO m
=> BlockHash
-> SqlPersistT m [NodeBlock]
getChildBlocks h = do
ch <- (+1) . nodeBlockHeight . fromMaybe e <$> getBlockByHash h
filter ((==h) . nodePrev) <$> getBlocksAtHeight ch
where
e = error $ "Cannot find block hash " ++ cs (blockHashToHex h)
-- | Get the last checkpoint that we have seen.
lastSeenCheckpoint :: MonadIO m
=> SqlPersistT m (Maybe NodeBlock)
lastSeenCheckpoint =
fmap listToMaybe $ getBlocksByHash $ map snd $ reverse checkpointList
-- | Returns the work required for a block header given the previous block. This
-- coresponds to bitcoind function GetNextWorkRequired in main.cpp.
nextWorkRequired :: NodeBlock
-> Timestamp
-> MinWork
-> BlockHeader
-> Word32
nextWorkRequired par ts mw bh
-- Genesis block
| nodeBlockHeight par == 0 = encodeCompact powLimit
-- Only change the difficulty once per interval
| (nodeBlockHeight par + 1) `mod` diffInterval /= 0 =
if allowMinDifficultyBlocks
then minPOW
else blockBits $ nHeader par
| otherwise = workFromInterval ts (nHeader par)
where
delta = targetSpacing * 2
minPOW
| blockTimestamp bh > nodeTimestamp par + delta = encodeCompact powLimit
| otherwise = mw
-- | 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 = fst $ decodeCompact $ blockBits lastB
newDiff = lastDiff * toInteger actualTime `div` toInteger targetTimespan
-- | 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 =
fromIntegral $ largestHash `div` (target + 1)
where
target = fst $ decodeCompact (blockBits bh)
largestHash = 1 `shiftL` 256
{- Persistent backend -}
chainPathQuery :: forall (expr :: * -> *) (query :: * -> *) backend.
Esqueleto query expr backend
=> expr (Entity NodeBlock)
-> [NodeBlock]
-> expr (Value Bool)
chainPathQuery _ [] = error "Monsters, monsters everywhere"
chainPathQuery t [NodeBlock{..}] =
t ^. NodeBlockHeight <=. val nodeBlockHeight &&.
t ^. NodeBlockChain ==. val nodeBlockChain
chainPathQuery t (n1:bs@(n2:_)) = chainPathQuery t bs ||.
( t ^. NodeBlockHeight <=. val (nodeBlockHeight n1)
&&. t ^. NodeBlockHeight >. val (nodeBlockHeight n2)
&&. t ^. NodeBlockChain ==. val (nodeBlockChain n1)
)
getHeads :: MonadIO m => SqlPersistT m [NodeBlock]
getHeads = fmap (map (entityVal . snd)) $ select $ from $ \t -> do
groupBy $ t ^. NodeBlockChain
return (max_ (t ^. NodeBlockHeight), t)
-- | Chain for new block building on a parent node
getChain :: MonadIO m
=> NodeBlock -- ^ Parent node
-> SqlPersistT m Word32
getChain parent = do
maxHeightM <- fmap (unValue <=< listToMaybe) $ select $ from $ \t -> do
where_ $ t ^. NodeBlockChain ==. val (nodeBlockChain parent)
return $ max_ $ t ^. NodeBlockHeight
let maxHeight = fromMaybe (error "That chain does not exist") maxHeightM
if maxHeight == nodeBlockHeight parent
then return $ nodeBlockChain parent
else do
maxChainM <- fmap (unValue <=< listToMaybe) $ select $ from $ \t ->
return $ max_ $ t ^. NodeBlockChain
let maxChain = fromMaybe (error "Ran out of chains") maxChainM
return $ maxChain + 1
getPivots :: MonadIO m => NodeBlock -> SqlPersistT m [NodeBlock]
getPivots = go []
where
go acc b
| nodeBlockChain b == 0 = return $ nGenesisBlock : b : acc
| otherwise = do
l <- fromMaybe (error "Houston, we have a problem") <$>
getChainLowest b
c <- fromMaybe (error "Ground Control to Major Tom") <$>
getParentBlock l
go (b:acc) c
getChainLowest :: MonadIO m => NodeBlock -> SqlPersistT m (Maybe NodeBlock)
getChainLowest nb = fmap (listToMaybe . map entityVal) $
select $ from $ \t -> do
where_ $ t ^. NodeBlockChain ==. val (nodeBlockChain nb)
orderBy [ asc $ t ^. NodeBlockHeight ]
limit 1
return t
-- | Get node height and chain common to both given.
splitBlock :: MonadIO m
=> NodeBlock
-> NodeBlock
-> SqlPersistT m NodeBlock
splitBlock l r = if nodeBlockChain l == nodeBlockChain r
then if nodeBlockHeight l < nodeBlockHeight r
then return l
else return r
else do
pivotsL <- getPivots l
pivotsR <- getPivots r
let ns = zip pivotsL pivotsR
f (x,y) = nodeBlockChain x == nodeBlockChain y
(one, two) = last $ takeWhile f ns
if nodeBlockHeight one < nodeBlockHeight two
then return one
else return two
-- | Put single block in database.
putBlock :: MonadIO m => NodeBlock -> SqlPersistT m ()
putBlock = insert_
-- | Put multiple blocks in database.
putBlocks :: MonadIO m => [NodeBlock] -> SqlPersistT m ()
putBlocks = mapM_ insertMany_ . f
where
f [] = []
f xs = let (xs',xxs) = splitAt 50 xs in xs' : f xxs
getBestBlock :: MonadIO m => SqlPersistT m NodeBlock
getBestBlock =
maximumBy (compare `on` nodeBlockWork) <$> getHeads
getBlockByHash :: MonadIO m => BlockHash -> SqlPersistT m (Maybe NodeBlock)
getBlockByHash h =
fmap (listToMaybe . map entityVal) $ select $ from $ \t -> do
where_ $ t ^. NodeBlockHash ==. val (shortHash h)
return t
-- | Get multiple blocks corresponding to given hashes
getBlocksByHash :: MonadIO m
=> [BlockHash]
-> SqlPersistT m [NodeBlock]
getBlocksByHash hashes = do
nodes <- fmap (map entityVal) $ select $ from $ \t -> do
where_ $ t ^. NodeBlockHash `in_` valList (map shortHash hashes)
return t
return $ mapMaybe
(\h -> find ((== shortHash h) . nodeBlockHash) nodes)
hashes
-- | Get ancestor of specified block at given height.
getBlockByHeight :: MonadIO m
=> NodeBlock -- ^ Best block
-> BlockHeight
-> SqlPersistT m (Maybe NodeBlock)
getBlockByHeight block height = do
forks <- reverse <$> getPivots block
fmap (listToMaybe . map entityVal) $ select $ from $ \t -> do
where_ $ chainPathQuery t forks &&.
t ^. NodeBlockHeight ==. val height
return t
-- | Get ancestors for specified block at given heights.
getBlocksByHeight :: MonadIO m
=> NodeBlock -- ^ Best block
-> [BlockHeight]
-> SqlPersistT m [NodeBlock]
getBlocksByHeight best heights = do
forks <- reverse <$> getPivots best
nodes <- fmap (map entityVal) $ select $ from $ \t -> do
where_ $ chainPathQuery t forks &&.
t ^. NodeBlockHeight `in_` valList heights
return t
return $ mapMaybe (\h -> find ((==h) . nodeBlockHeight) nodes) heights
-- | Get a range of block headers building up to specified block. If
-- specified height is too large, an empty list will be returned.
getBlocksFromHeight :: MonadIO m
=> NodeBlock -- ^ Best block
-> Word32 -- ^ Count (0 for all)
-> BlockHeight -- ^ Height from (including)
-> SqlPersistT m [NodeBlock]
getBlocksFromHeight block cnt height = do
forks <- reverse <$> getPivots block
fmap (map entityVal) $ select $ from $ \t -> do
where_ $ chainPathQuery t forks &&.
t ^. NodeBlockHeight >=. val height
when (cnt > 0) $ limit $ fromIntegral cnt
return t
-- | Get node immediately at or after timestamp in main chain.
getBlockAfterTime :: MonadIO m => Timestamp -> SqlPersistT m (Maybe NodeBlock)
getBlockAfterTime ts = do
n@NodeBlock{..} <- getBestBlock
f nGenesisBlock n
where
f l r | nodeTimestamp r < ts =
return Nothing
| nodeTimestamp l >= ts =
return $ Just l
| (nodeBlockHeight r - nodeBlockHeight l) `div` 2 == 0 =
return $ Just r
| otherwise = do
let rh = nodeBlockHeight r
lh = nodeBlockHeight l
mh = rh - (rh - lh) `div` 2
m <- fromMaybe (error "My God, its full of stars!") <$>
getBlockByHeight r mh
if nodeTimestamp m > ts then f l m else f m r
-- | Get blocks at specified height in all chains.
getBlocksAtHeight :: MonadIO m => BlockHeight -> SqlPersistT m [NodeBlock]
getBlocksAtHeight height = fmap (map entityVal) $ select $ from $ \t -> do
where_ $ t ^. NodeBlockHeight ==. val height
return t
-- | Evaluate block action for provided best block and chain of new blocks.
evalNewChain :: MonadIO m
=> NodeBlock
-> [NodeBlock]
-> SqlPersistT m BlockChainAction
evalNewChain _ [] = error "You find yourself in the dungeon of missing blocks"
evalNewChain best newNodes
| buildsOnBest =
return $ BestChain newNodes
| nodeBlockWork (last newNodes) > nodeBlockWork best = do
(split, old, new) <- splitChains (best, 0) (head newNodes, 0)
return $ ChainReorg split old (new ++ tail newNodes)
| otherwise = do
(split, _, new) <- splitChains (best, 0) (head newNodes, 0)
case new of
[] -> return $ KnownChain newNodes
_ -> return $ SideChain $ split : new ++ tail newNodes
where
buildsOnBest = nodePrev (head newNodes) == nodeHash best
-- | Remove all other chains from database and return updated best block node.
pruneChain :: MonadIO m
=> NodeBlock
-> SqlPersistT m NodeBlock
pruneChain best = if nodeBlockChain best == 0 then return best else do
forks <- reverse <$> getPivots best
delete $ from $ \t -> where_ $ not_ (chainPathQuery t forks)
update $ \t -> do
set t [ NodeBlockChain =. val 0 ]
where_ $ t ^. NodeBlockHeight <=. val (nodeBlockHeight best)
&&. t ^. NodeBlockChain !=. val 0
return best{ nodeBlockChain = 0 }

View File

@ -1,28 +0,0 @@
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
module Network.Haskoin.Node.HeaderTree.Model where
import Data.Word (Word32)
import Database.Persist.TH (mkMigrate, mkPersist,
persistLowerCase, share,
sqlSettings)
import Network.Haskoin.Block
import Network.Haskoin.Node.HeaderTree.Types
share [mkPersist sqlSettings, mkMigrate "migrateHeaderTree"] [persistLowerCase|
NodeBlock
hash ShortHash
header NodeHeader maxlen=80
work Work
height BlockHeight
chain Word32
UniqueHash hash
UniqueChain chain height
deriving Show
deriving Eq
|]

View File

@ -1,28 +0,0 @@
module Network.Haskoin.Node.HeaderTree.Types where
import Data.Serialize (decode, encode)
import Data.String (fromString)
import Data.Word (Word64)
import Database.Persist (PersistField (..), PersistValue (..),
SqlType (..))
import Database.Persist.Sql (PersistFieldSql (..))
import Network.Haskoin.Block
type ShortHash = Word64
type Work = Double
newtype NodeHeader = NodeHeader { getNodeHeader :: BlockHeader }
deriving (Show, Eq)
{- SQL database backend for HeaderTree -}
instance PersistField NodeHeader where
toPersistValue = PersistByteString . encode . getNodeHeader
fromPersistValue (PersistByteString bs) =
case decode bs of
Right x -> Right (NodeHeader x)
Left e -> Left (fromString e)
fromPersistValue _ = Left "Invalid persistent block header"
instance PersistFieldSql NodeHeader where
sqlType _ = SqlBlob

View File

@ -1,720 +0,0 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
module Network.Haskoin.Node.Peer where
import Control.Concurrent (killThread, myThreadId,
threadDelay)
import Control.Concurrent.Async.Lifted (link, race, waitAnyCancel,
waitCatch, withAsync)
import Control.Concurrent.STM (STM, atomically, modifyTVar',
newTVarIO, readTVar, retry,
swapTVar)
import Control.Concurrent.STM.TBMChan (TBMChan, closeTBMChan,
newTBMChan, writeTBMChan)
import Control.Exception (AsyncException(ThreadKilled))
import Control.Exception.Lifted (finally, fromException, throw,
throwIO)
import Control.Monad (forM_, forever, join, unless,
when)
import Control.Monad.Logger (MonadLoggerIO, logDebug,
logError, logInfo, logWarn)
import Control.Monad.Reader (asks)
import Control.Monad.State (StateT, evalStateT, get, put)
import Control.Monad.Trans (MonadIO, lift, liftIO)
import Control.Monad.Trans.Control (MonadBaseControl)
import Data.Bits (testBit)
import qualified Data.ByteString as BS (ByteString, append,
null)
import qualified Data.ByteString.Char8 as C (pack)
import qualified Data.ByteString.Lazy as BL (toStrict)
import Data.Conduit (Conduit, Sink, awaitForever,
yield, ($$), ($=))
import qualified Data.Conduit.Binary as CB (take)
import Data.Conduit.Network (appSink, appSource,
clientSettings,
runGeneralTCPClient)
import Data.Conduit.TMChan (sourceTBMChan)
import Data.List (nub, sort, sortBy)
import qualified Data.Map as M (assocs, elems, fromList,
keys, lookup, unionWith)
import Data.Maybe (fromMaybe, isJust,
listToMaybe)
import Data.Serialize (decode, encode)
import Data.String.Conversions (cs)
import Data.Text (Text, pack)
import Data.Time.Clock (diffUTCTime, getCurrentTime)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Data.Unique (hashUnique, newUnique)
import Data.Word (Word32)
import Network.Haskoin.Block
import Network.Haskoin.Constants
import Network.Haskoin.Network
import Network.Haskoin.Node.HeaderTree
import Network.Haskoin.Node.STM
import Network.Haskoin.Transaction
import Network.Haskoin.Util
import Network.Socket (SockAddr (SockAddrInet))
import System.Random (randomIO)
-- TODO: Move constants elsewhere ?
minProtocolVersion :: Word32
minProtocolVersion = 70001
-- Start a reconnecting peer that will idle once the connection is established
-- and the handshake is performed.
startPeer :: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerHost
-> NodeT m ()
startPeer ph@PeerHost{..} = do
-- Create a new unique ID for this peer
pid <- liftIO newUnique
-- Start the peer with the given PID
startPeerPid pid ph
-- Start a peer that will try to reconnect when the connection is closed. The
-- reconnections are performed using an expoential backoff time. This function
-- blocks until the peer cannot reconnect (either the peer is banned or we
-- already have a peer connected to the given peer host).
startReconnectPeer :: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerHost
-> NodeT m ()
startReconnectPeer ph@PeerHost{..} = do
-- Create a new unique ID for this peer
pid <- liftIO newUnique
-- Wait if there is a reconnection timeout
maybeWaitReconnect pid
-- Launch the peer
withAsync (startPeerPid pid ph) $ \a -> do
resE <- liftIO $ waitCatch a
reconnect <- case resE of
Left se -> do
$(logError) $ formatPid pid ph $ unwords
[ "Peer thread stopped with exception:", show se ]
return $ case fromException se of
Just NodeExceptionBanned -> False
Just NodeExceptionConnected -> False
Just (NodeExceptionInvalidPeer _) -> False
_ -> fromException se /= Just ThreadKilled
Right _ -> do
$(logDebug) $ formatPid pid ph "Peer thread stopped"
return True
-- Try to reconnect
when reconnect $ startReconnectPeer ph
where
maybeWaitReconnect pid = do
reconnect <- atomicallyNodeT $ do
sessM <- getHostSession ph
case sessM of
Just PeerHostSession{..} -> do
-- Compute the new reconnection time (max 15 minutes)
let reconnect = min 900 $ 2 * peerHostSessionReconnect
-- Save the reconnection time
modifyHostSession ph $ \s ->
s{ peerHostSessionReconnect = reconnect }
return reconnect
_ -> return 0
when (reconnect > 0) $ do
$(logInfo) $ formatPid pid ph $ unwords
[ "Reconnecting peer in", show reconnect, "seconds" ]
-- Wait for some time before calling a reconnection
liftIO $ threadDelay $ reconnect * 1000000
-- Start a peer with with the given peer host/peer id and initiate the
-- network protocol handshake. This function will block until the peer
-- connection is closed or an exception is raised.
startPeerPid :: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerId
-> PeerHost
-> NodeT m ()
startPeerPid pid ph@PeerHost{..} = do
-- Check if the peer host is banned
banned <- atomicallyNodeT $ isPeerHostBanned ph
when banned $ do
$(logWarn) $ formatPid pid ph "Failed to start banned host"
liftIO $ throwIO NodeExceptionBanned
-- Check if the peer host is already connected
connected <- atomicallyNodeT $ isPeerHostConnected ph
when connected $ do
$(logWarn) $ formatPid pid ph "This host is already connected"
liftIO $ throwIO NodeExceptionConnected
tid <- liftIO myThreadId
chan <- liftIO . atomically $ newTBMChan 1024
mChan <- liftIO . atomically $ newTBMChan 1024
pings <- liftIO $ newTVarIO []
atomicallyNodeT $ do
newPeerSession pid PeerSession
{ peerSessionConnected = False
, peerSessionVersion = Nothing
, peerSessionHeight = 0
, peerSessionChan = chan
, peerSessionHost = ph
, peerSessionThreadId = tid
, peerSessionMerkleChan = mChan
, peerSessionPings = pings
, peerSessionScore = Nothing
}
newHostSession ph PeerHostSession
{ peerHostSessionScore = 0
, peerHostSessionReconnect = 1
, peerHostSessionLog = []
}
$(logDebug) $ formatPid pid ph "Starting a new client TCP connection"
-- Start the client TCP connection
let c = clientSettings peerPort $ C.pack peerHost
runGeneralTCPClient c (peerTCPClient chan) `finally` cleanupPeer
return ()
where
peerTCPClient chan ad = do
-- Conduit for receiving messages from the remote host
let recvMsg = appSource ad $$ decodeMessage pid ph
-- Conduit for sending messages to the remote host
sendMsg = sourceTBMChan chan $= encodeMessage $$ appSink ad
withAsync (evalStateT recvMsg Nothing) $ \a1 -> link a1 >> do
$(logDebug) $ formatPid pid ph
"Receiving message thread started..."
withAsync sendMsg $ \a2 -> link a2 >> do
$(logDebug) $ formatPid pid ph
"Sending message thread started..."
-- Perform the peer handshake before we continue
-- Timeout after 2 minutes
resE <- raceTimeout 120 (disconnectPeer pid ph)
(peerHandshake pid ph chan)
case resE of
Left _ -> $(logError) $ formatPid pid ph
"Peer timed out during the connection handshake"
_ -> do
-- Send the bloom filter if we have one
$(logDebug) $ formatPid pid ph
"Sending the bloom filter if we have one"
atomicallyNodeT $ do
bloomM <- readTVarS sharedBloomFilter
case bloomM of
Just (bloom, _) ->
sendMessage pid $
MFilterLoad $ FilterLoad bloom
_ -> return ()
withAsync (peerPing pid ph) $ \a3 -> link a3 >> do
$(logDebug) $ formatPid pid ph "Ping thread started"
_ <- liftIO $ waitAnyCancel [a1, a2, a3]
$(logDebug) $ formatPid pid ph "Exiting peer TCP thread"
return ()
cleanupPeer = do
$(logWarn) $ formatPid pid ph "Peer is closing. Running cleanup..."
atomicallyNodeT $ do
-- Remove the header syncing peer if necessary
hPidM <- readTVarS sharedHeaderPeer
when (hPidM == Just pid) $ writeTVarS sharedHeaderPeer Nothing
-- Remove the merkle syncing peer if necessary
mPidM <- readTVarS sharedMerklePeer
when (mPidM == Just pid) $ writeTVarS sharedMerklePeer Nothing
-- Remove the session and close the channels
sessM <- removePeerSession pid
case sessM of
Just PeerSession{..} -> lift $ do
closeTBMChan peerSessionChan
closeTBMChan peerSessionMerkleChan
_ -> return ()
-- Update the network height
updateNetworkHeight
-- Return True if the PeerHost is banned
isPeerHostBanned :: PeerHost -> NodeT STM Bool
isPeerHostBanned ph = do
hostMap <- readTVarS sharedHostMap
case M.lookup ph hostMap of
Just sessTVar -> do
sess <- lift $ readTVar sessTVar
return $ isHostScoreBanned $ peerHostSessionScore sess
_ -> return False
-- Returns True if we have a peer connected to that PeerHost already
isPeerHostConnected :: PeerHost -> NodeT STM Bool
isPeerHostConnected ph = do
peerMap <- readTVarS sharedPeerMap
sess <- lift $ mapM readTVar $ M.elems peerMap
return $ ph `elem` map peerSessionHost sess
-- | Decode messages sent from the remote host and send them to the peers main
-- message queue for processing. If we receive invalid messages, this function
-- will also notify the PeerManager about a misbehaving remote host.
decodeMessage
:: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerId
-> PeerHost
-> Sink BS.ByteString (StateT (Maybe (MerkleBlock, MerkleTxs)) (NodeT m)) ()
decodeMessage pid ph = do
-- Message header is always 24 bytes
headerBytes <- BL.toStrict <$> CB.take 24
-- If headerBytes is empty, the conduit has disconnected and we need to
-- exit (not recurse). Otherwise, we go into an infinite loop here.
unless (BS.null headerBytes) $ do
-- Introspection required to know the length of the payload
case decode headerBytes of
Left err -> lift . lift $ misbehaving pid ph moderateDoS $ unwords
[ "Could not decode message header:", err
, "Bytes:", cs (encodeHex headerBytes)
]
Right (MessageHeader _ cmd len _) -> do
$(logDebug) $ formatPid pid ph $ unwords
[ "Received message header of type", show cmd ]
payloadBytes <- BL.toStrict <$> CB.take (fromIntegral len)
case decode $ headerBytes `BS.append` payloadBytes of
Left err -> lift . lift $ misbehaving pid ph moderateDoS $
unwords [ "Could not decode message payload:", err ]
Right msg -> lift $ processMessage pid ph msg
decodeMessage pid ph
-- Handle a message from a peer
processMessage :: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerId
-> PeerHost
-> Message
-> StateT (Maybe (MerkleBlock, MerkleTxs)) (NodeT m) ()
processMessage pid ph msg = checkMerkleEnd >> case msg of
MVersion v -> lift $ do
$(logDebug) $ formatPid pid ph "Processing MVersion message"
join . atomicallyNodeT $ do
oldVerM <- peerSessionVersion <$> getPeerSession pid
case oldVerM of
Just _ -> do
_ <- trySendMessage pid $ MReject $ reject
MCVersion RejectDuplicate "Duplicate version message"
return $
misbehaving pid ph minorDoS "Duplicate version message"
Nothing -> do
modifyPeerSession pid $ \s ->
s{ peerSessionVersion = Just v }
return $ return ()
$(logDebug) $ formatPid pid ph "Done processing MVersion message"
MPing (Ping nonce) -> lift $ do
$(logDebug) $ formatPid pid ph "Processing MPing message"
-- Just reply to the Ping with a Pong message
_ <- atomicallyNodeT $ trySendMessage pid $ MPong $ Pong nonce
return ()
MPong (Pong nonce) -> lift $ do
$(logDebug) $ formatPid pid ph "Processing MPong message"
atomicallyNodeT $ do
PeerSession{..} <- getPeerSession pid
-- Add the Pong response time
lift $ modifyTVar' peerSessionPings (++ [nonce])
MHeaders h -> lift $ do
$(logDebug) $ formatPid pid ph "Processing MHeaders message"
_ <- atomicallyNodeT $ tryPutTMVarS sharedHeaders (pid, h)
return ()
MInv inv -> lift $ do
$(logDebug) $ formatPid pid ph "Processing MInv message"
processInvMessage pid ph inv
MGetData (GetData inv) -> do
$(logDebug) $ formatPid pid ph "Processing MGetData message"
let txlist = filter ((== InvTx) . invType) inv
txids = nub $ map (TxHash . invHash) txlist
$(logDebug) $ formatPid pid ph $ unlines $
"Received GetData request for transactions"
: map ((" " ++) . cs . txHashToHex) txids
-- Add the txids to the GetData request map
mapTVar <- asks sharedTxGetData
liftIO . atomically $ modifyTVar' mapTVar $ \datMap ->
let newMap = M.fromList $ map (\tid -> (tid, [(pid, ph)])) txids
in M.unionWith (\x -> nub . (x ++)) newMap datMap
MTx tx -> do
$(logDebug) $ formatPid pid ph "Processing MTx message"
PeerSession{..} <- lift . atomicallyNodeT $ getPeerSession pid
txChan <- lift $ asks sharedTxChan
get >>= \merkleM -> case merkleM of
Just (_, mTxs) -> if txHash tx `elem` mTxs
then do
$(logDebug) $ formatPid pid ph $ unwords
[ "Received merkle tx", cs $ txHashToHex $ txHash tx ]
liftIO . atomically $
writeTBMChan peerSessionMerkleChan $ Right tx
else do
$(logDebug) $ formatPid pid ph $ unwords
[ "Received tx broadcast (ending a merkle block)"
, cs $ txHashToHex $ txHash tx
]
endMerkle
liftIO . atomically $ writeTBMChan txChan (pid, ph, tx)
_ -> do
$(logDebug) $ formatPid pid ph $ unwords
[ "Received tx broadcast", cs $ txHashToHex $ txHash tx ]
liftIO . atomically $ writeTBMChan txChan (pid, ph, tx)
MMerkleBlock mb@(MerkleBlock mHead ntx hs fs) -> do
$(logDebug) $ formatPid pid ph "Processing MMerkleBlock message"
case extractMatches fs hs (fromIntegral ntx) of
Left err -> lift $ misbehaving pid ph severeDoS $ unwords
[ "Received an invalid merkle block:", err ]
Right (decodedRoot, mTxs) ->
-- Make sure that the merkle roots match
if decodedRoot == merkleRoot mHead
then do
$(logDebug) $ formatPid pid ph $ unwords
[ "Received valid merkle block"
, cs $ blockHashToHex $ headerHash mHead
]
forM_ mTxs $ \h ->
$(logDebug) $ formatPid pid ph $ unwords
[ "Matched merkle tx:", cs $ txHashToHex h ]
if null mTxs
-- Deliver the merkle block
then lift . atomicallyNodeT $ do
PeerSession{..} <- getPeerSession pid
lift $ writeTBMChan peerSessionMerkleChan $
Left (mb, [])
-- Buffer the merkle block until we received all txs
else put $ Just (mb, mTxs)
else lift $ misbehaving pid ph severeDoS
"Received a merkle block with an invalid merkle root"
_ -> return () -- Ignore other requests
where
checkMerkleEnd = unless (isTxMsg msg) endMerkle
endMerkle = get >>= \merkleM -> case merkleM of
Just (mb, mTxs) -> do
lift . atomicallyNodeT $ do
PeerSession{..} <- getPeerSession pid
lift $ writeTBMChan peerSessionMerkleChan $ Left (mb, mTxs)
put Nothing
_ -> return ()
isTxMsg (MTx _) = True
isTxMsg _ = False
processInvMessage :: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerId
-> PeerHost
-> Inv
-> NodeT m ()
processInvMessage pid ph (Inv vs) = case tickleM of
Just tickle -> do
$(logDebug) $ formatPid pid ph $ unwords
[ "Received block tickle", cs $ blockHashToHex tickle ]
tickleChan <- asks sharedTickleChan
liftIO $ atomically $ writeTBMChan tickleChan (pid, ph, tickle)
_ -> do
unless (null txlist) $ do
forM_ txlist $ \tid -> $(logDebug) $ formatPid pid ph $ unwords
[ "Received transaction INV", cs (txHashToHex tid) ]
-- We simply request the transactions.
-- TODO: Should we do something more elaborate here?
atomicallyNodeT $ sendMessage pid $ MGetData $ GetData $
map (InvVector InvTx . getTxHash) txlist
unless (null blocklist) $ do
$(logDebug) $ formatPid pid ph $ unlines $
"Received block INV"
: map ((" " ++) . cs . blockHashToHex) blocklist
-- We ignore block INVs as we do headers-first sync
return ()
where
-- Single blockhash INV is a tickle
tickleM = case blocklist of
[h] -> if null txlist then Just h else Nothing
_ -> Nothing
txlist :: [TxHash]
txlist = map (TxHash . invHash) $
filter ((== InvTx) . invType) vs
blocklist :: [BlockHash]
blocklist = map (BlockHash . invHash) $ filter ((== InvBlock) . invType) vs
-- | Encode message that are being sent to the remote host.
encodeMessage :: MonadLoggerIO m
=> Conduit Message (NodeT m) BS.ByteString
encodeMessage = awaitForever $ yield . encode
peerPing :: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerId
-> PeerHost
-> NodeT m ()
peerPing pid ph = forever $ do
$(logDebug) $ formatPid pid ph
"Waiting until the peer is available for sending pings..."
atomicallyNodeT $ waitPeerAvailable pid
nonce <- liftIO randomIO
nonceTVar <- atomicallyNodeT $ do
PeerSession{..} <- getPeerSession pid
sendMessage pid $ MPing $ Ping nonce
return peerSessionPings
$(logDebug) $ formatPid pid ph $ unwords
[ "Waiting for Ping nonce", show nonce ]
-- Wait 120 seconds for the pong or time out
startTime <- liftIO getCurrentTime
resE <- raceTimeout 120 (killPeer nonce) (waitPong nonce nonceTVar)
case resE of
Right _ -> do
endTime <- liftIO getCurrentTime
(diff, score) <- atomicallyNodeT $ do
PeerSession{..} <- getPeerSession pid
-- Compute the ping time and the new score
let diff = diffUTCTime endTime startTime
score = 0.5 * diff + 0.5 * fromMaybe diff peerSessionScore
-- Save the score in the peer session unless the peer is busy
modifyPeerSession pid $ \s -> s{ peerSessionScore = Just score }
return (diff, score)
$(logDebug) $ formatPid pid ph $ unwords
[ "Got response to ping", show nonce
, "with time", show diff, "and score", show score
]
_ -> return ()
-- Sleep 30 seconds before sending the next ping
liftIO $ threadDelay $ 30 * 1000000
where
-- Wait for the Pong message of our Ping nonce to arrive
waitPong nonce nonceTVar = do
ns <- liftIO . atomically $ do
ns <- swapTVar nonceTVar []
if null ns then retry else return ns
unless (nonce `elem` ns) $ waitPong nonce nonceTVar
killPeer nonce = do
$(logWarn) $ formatPid pid ph $ concat
[ "Did not receive a timely reply for Ping ", show nonce
, ". Reconnecting the peer."
]
disconnectPeer pid ph
isBloomDisabled :: Version -> Bool
isBloomDisabled ver = version ver >= 70011 && not (services ver `testBit` 2)
peerHandshake :: (MonadLoggerIO m, MonadBaseControl IO m)
=> PeerId
-> PeerHost
-> TBMChan Message
-> NodeT m ()
peerHandshake pid ph chan = do
ourVer <- buildVersion
$(logDebug) $ formatPid pid ph "Sending our version message"
liftIO . atomically $ writeTBMChan chan $ MVersion ourVer
-- Wait for the peer version message to arrive
$(logDebug) $ formatPid pid ph "Waiting for the peers version message..."
peerVer <- atomicallyNodeT $ waitPeerVersion pid
$(logInfo) $ formatPid pid ph $ unlines
[ unwords [ "Connected to peer host"
, show $ naAddress $ addrSend peerVer
]
, unwords [ " version :", show $ version peerVer ]
, unwords [ " subVer :", show $ userAgent peerVer ]
, unwords [ " services :", show $ services peerVer ]
, unwords [ " time :", show $ timestamp peerVer ]
, unwords [ " blocks :", show $ startHeight peerVer ]
]
-- Check the protocol version
go peerVer $ do
atomicallyNodeT $ do
-- Save the peers height and update the network height
modifyPeerSession pid $ \s ->
s{ peerSessionHeight = startHeight peerVer
, peerSessionConnected = True
}
updateNetworkHeight
-- Reset the reconnection timer (exponential backoff)
modifyHostSession ph $ \s ->
s{ peerHostSessionReconnect = 1 }
-- ACK the version message
lift $ writeTBMChan chan MVerAck
$(logDebug) $ formatPid pid ph "Handshake complete"
where
go ver action
| version ver < minProtocolVersion =
misbehaving pid ph severeDoS $ unwords
[ "Connected to a peer speaking protocol version"
, show $ version ver
, "but we require at least"
, show minProtocolVersion
]
| isBloomDisabled ver =
misbehaving pid ph severeDoS "Peer does not support bloom filters"
| otherwise = action
buildVersion = do
-- TODO: Get our correct IP here
let add = NetworkAddress 1 $ SockAddrInet 0 0
ua = VarString haskoinUserAgent
time <- floor <$> liftIO getPOSIXTime
rdmn <- liftIO randomIO -- nonce
height <- nodeBlockHeight <$> atomicallyNodeT (readTVarS sharedBestHeader)
return Version { version = 70011
, services = 5
, timestamp = time
, addrRecv = add
, addrSend = add
, verNonce = rdmn
, userAgent = ua
, startHeight = height
, relay = False
}
-- Wait for the version message of a peer and return it
waitPeerVersion :: PeerId -> NodeT STM Version
waitPeerVersion pid = do
PeerSession{..} <- getPeerSession pid
case peerSessionVersion of
Just ver -> return ver
_ -> lift retry
-- Delete the session of a peer and send a kill signal to the peers thread.
-- Unless the peer is banned, the peer will try to reconnect.
disconnectPeer :: (MonadLoggerIO m)
=> PeerId
-> PeerHost
-> NodeT m ()
disconnectPeer pid ph = do
sessM <- atomicallyNodeT $ tryGetPeerSession pid
case sessM of
Just PeerSession{..} -> do
$(logDebug) $ formatPid pid ph "Killing the peer thread"
liftIO $ killThread peerSessionThreadId
_ -> return ()
{- Peer utility functions -}
--- Wait until the given peer is not syncing headers or merkle blocks
waitPeerAvailable :: PeerId -> NodeT STM ()
waitPeerAvailable pid = do
hPidM <- readTVarS sharedHeaderPeer
mPidM <- readTVarS sharedMerklePeer
when (Just pid `elem` [hPidM, mPidM]) $ lift retry
-- Wait for a non-empty bloom filter to be available
waitBloomFilter :: NodeT STM BloomFilter
waitBloomFilter =
maybe (lift retry) (return . fst) =<< readTVarS sharedBloomFilter
sendBloomFilter :: BloomFilter -> Int -> NodeT STM ()
sendBloomFilter bloom elems = unless (isBloomEmpty bloom) $ do
oldBloomM <- readTVarS sharedBloomFilter
let oldElems = maybe 0 snd oldBloomM
-- Only update the bloom filter if the number of elements is larger
when (elems > oldElems) $ do
writeTVarS sharedBloomFilter $ Just (bloom, elems)
sendMessageAll $ MFilterLoad $ FilterLoad bloom
-- Returns the median height of all the peers
getMedianHeight :: NodeT STM BlockHeight
getMedianHeight = do
hs <- map (peerSessionHeight . snd) <$> getConnectedPeers
let (_,ms) = splitAt (length hs `div` 2) $ sort hs
return $ fromMaybe 0 $ listToMaybe ms
-- Set the network height to the median height of all peers.
updateNetworkHeight :: NodeT STM ()
updateNetworkHeight = writeTVarS sharedNetworkHeight =<< getMedianHeight
getPeers :: NodeT STM [(PeerId, PeerSession)]
getPeers = do
peerMap <- readTVarS sharedPeerMap
lift $ mapM f $ M.assocs peerMap
where
f (pid, sess) = (,) pid <$> readTVar sess
getConnectedPeers :: NodeT STM [(PeerId, PeerSession)]
getConnectedPeers = filter (peerSessionConnected . snd) <$> getPeers
-- Returns a peer that is connected, at the network height and
-- with the best score.
getPeersAtNetHeight :: NodeT STM [(PeerId, PeerSession)]
getPeersAtNetHeight = do
-- Find the current network height
height <- readTVarS sharedNetworkHeight
getPeersAtHeight (== height)
-- Find the best peer at the given height
getPeersAtHeight :: (BlockHeight -> Bool)
-> NodeT STM [(PeerId, PeerSession)]
getPeersAtHeight cmpHeight = do
peers <- filter f <$> getPeers
-- Choose the peer with the best score
return $ sortBy s peers
where
f (_, p) =
peerSessionConnected p && -- Only connected peers
isJust (peerSessionScore p) && -- Only peers with scores
cmpHeight (peerSessionHeight p) -- Only peers at the required height
s (_,a) (_,b) = peerSessionScore a `compare` peerSessionScore b
-- Send a message to a peer only if it is connected. It returns True on
-- success.
trySendMessage :: PeerId -> Message -> NodeT STM Bool
trySendMessage pid msg = do
sessM <- tryGetPeerSession pid
lift $ case sessM of
Just PeerSession{..} ->
if peerSessionConnected
then writeTBMChan peerSessionChan msg >> return True
else return False -- The peer is not yet connected
_ -> return False -- The peer does not exist
-- Send a message to a peer only if it is connected. It returns True on
-- success. Throws an exception if the peer does not exist or is not connected.
sendMessage :: PeerId -> Message -> NodeT STM ()
sendMessage pid msg = do
PeerSession{..} <- getPeerSession pid
if peerSessionConnected
then lift $ writeTBMChan peerSessionChan msg
else throw $ NodeExceptionPeerNotConnected $ ShowPeerId pid
-- Send a message to all connected peers.
sendMessageAll :: Message -> NodeT STM ()
sendMessageAll msg = do
peerMap <- readTVarS sharedPeerMap
forM_ (M.keys peerMap) $ \pid -> trySendMessage pid msg
getNetworkHeight :: NodeT STM BlockHeight
getNetworkHeight = readTVarS sharedNetworkHeight
misbehaving :: (MonadLoggerIO m)
=> PeerId
-> PeerHost
-> (PeerHostScore -> PeerHostScore)
-> String
-> NodeT m ()
misbehaving pid ph f msg = do
sessM <- atomicallyNodeT $ do
modifyHostSession ph $ \s ->
s{ peerHostSessionScore = f $! peerHostSessionScore s
, peerHostSessionLog = msg : peerHostSessionLog s
}
getHostSession ph
case sessM of
Just PeerHostSession{..} -> do
$(logWarn) $ formatPid pid ph $ unlines
[ "Misbehaving peer"
, unwords [ " Score:", show peerHostSessionScore ]
, unwords [ " Reason:", msg ]
]
when (isHostScoreBanned peerHostSessionScore) $
disconnectPeer pid ph
_ -> return ()
{- Run header tree database action -}
-- runHeaderTree :: MonadIO m => ReaderT L.DB IO a -> NodeT m a
-- runHeaderTree action = undefined
{- Utilities -}
raceTimeout :: (MonadIO m, MonadBaseControl IO m)
=> Int
-- ^ Timeout value in seconds
-> m a
-- ^ Action to run if the main action times out
-> m b
-- ^ Action to run until the time runs out
-> m (Either a b)
raceTimeout sec cleanup action = do
resE <- race (liftIO $ threadDelay (sec * 1000000)) action
case resE of
Right res -> return $ Right res
Left _ -> fmap Left cleanup
formatPid :: PeerId -> PeerHost -> String -> Text
formatPid pid ph str = pack $ concat
[ "[Peer ", show $ hashUnique pid
, " | ", peerHostString ph, "] ", str
]

View File

@ -1,412 +0,0 @@
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
module Network.Haskoin.Node.STM where
import Control.Concurrent (ThreadId)
import Control.Concurrent.STM (STM, TMVar, TVar, atomically,
isEmptyTMVar, modifyTVar',
newEmptyTMVarIO, newTVar,
newTVarIO, orElse, putTMVar,
readTMVar, readTVar,
takeTMVar, tryPutTMVar,
tryReadTMVar, writeTVar)
import Control.Concurrent.STM.Lock (Lock)
import qualified Control.Concurrent.STM.Lock as Lock (new)
import Control.Concurrent.STM.TBMChan (TBMChan, closeTBMChan,
newTBMChan)
import Control.DeepSeq (NFData (..))
import Control.Exception.Lifted (Exception, SomeException,
catch, fromException, throw)
import Control.Monad ((<=<))
import Control.Monad.Logger (MonadLoggerIO, logDebug)
import Control.Monad.Reader (ReaderT, ask, asks,
runReaderT)
import Control.Monad.Trans (MonadIO, lift, liftIO)
import Control.Monad.Trans.Control (MonadBaseControl)
import Data.Aeson.TH (deriveJSON)
import qualified Data.Map.Strict as M (Map, delete, empty,
insert, lookup)
import Data.Maybe (isJust)
import Data.Time.Clock (NominalDiffTime)
import Data.Typeable (Typeable)
import Data.Unique (Unique, hashUnique)
import Data.Word (Word32, Word64)
import Database.Persist.Sql (ConnectionPool, SqlBackend,
SqlPersistT, runSqlConn,
runSqlPool)
import Network.Haskoin.Block
import Network.Haskoin.Network
import Network.Haskoin.Node.HeaderTree
import Network.Haskoin.Transaction
import Network.Haskoin.Util
{- Type aliases -}
type MerkleTxs = [TxHash]
type NodeT = ReaderT SharedNodeState
type PeerId = Unique
type PeerHostScore = Word32
newtype ShowPeerId = ShowPeerId { getShowPeerId :: PeerId }
deriving (Eq)
instance Show ShowPeerId where
show = show . hashUnique . getShowPeerId
runSql :: (MonadBaseControl IO m)
=> SqlPersistT m a
-> Either SqlBackend ConnectionPool
-> m a
runSql f (Left conn) = runSqlConn f conn
runSql f (Right pool) = runSqlPool f pool
runSqlNodeT :: (MonadBaseControl IO m) => SqlPersistT m a -> NodeT m a
runSqlNodeT f = asks sharedSqlBackend >>= lift . runSql f
getNodeState :: (MonadLoggerIO m, MonadBaseControl IO m)
=> Either SqlBackend ConnectionPool
-> m SharedNodeState
getNodeState sharedSqlBackend = do
-- Initialize the HeaderTree
$(logDebug) "Initializing the HeaderTree and NodeState"
best <- runSql (initHeaderTree >> getBestBlock) sharedSqlBackend
liftIO $ do
sharedPeerMap <- newTVarIO M.empty
sharedHostMap <- newTVarIO M.empty
sharedNetworkHeight <- newTVarIO 0
sharedHeaders <- newEmptyTMVarIO
sharedHeaderPeer <- newTVarIO Nothing
sharedMerklePeer <- newTVarIO Nothing
sharedSyncLock <- atomically Lock.new
sharedTickleChan <- atomically $ newTBMChan 1024
sharedTxChan <- atomically $ newTBMChan 1024
sharedTxGetData <- newTVarIO M.empty
sharedRescan <- newEmptyTMVarIO
sharedMempool <- newTVarIO False
sharedBloomFilter <- newTVarIO Nothing
-- Find our best node in the HeaderTree
sharedBestHeader <- newTVarIO best
sharedBestBlock <- newTVarIO nGenesisBlock
return SharedNodeState{..}
runNodeT :: Monad m => NodeT m a -> SharedNodeState -> m a
runNodeT = runReaderT
withNodeT :: (MonadLoggerIO m, MonadBaseControl IO m)
=> NodeT m a
-> Either SqlBackend ConnectionPool
-> m a
withNodeT action sql = runNodeT action =<< getNodeState sql
atomicallyNodeT :: MonadIO m => NodeT STM a -> NodeT m a
atomicallyNodeT action = liftIO . atomically . runReaderT action =<< ask
{- PeerHost Session -}
data PeerHostSession = PeerHostSession
{ peerHostSessionScore :: !PeerHostScore
, peerHostSessionReconnect :: !Int
, peerHostSessionLog :: ![String]
-- ^ Important host log messages that should appear in status command
}
instance NFData PeerHostSession where
rnf PeerHostSession{..} =
rnf peerHostSessionScore `seq`
rnf peerHostSessionReconnect `seq`
rnf peerHostSessionLog
{- Shared Peer STM Type -}
data SharedNodeState = SharedNodeState
{ sharedPeerMap :: !(TVar (M.Map PeerId (TVar PeerSession)))
-- ^ Map of all active peers and their sessions
, sharedHostMap :: !(TVar (M.Map PeerHost (TVar PeerHostSession)))
-- ^ The peer that is currently syncing the block headers
, sharedNetworkHeight :: !(TVar BlockHeight)
-- ^ The current height of the network
, sharedHeaders :: !(TMVar (PeerId, Headers))
-- ^ Block headers sent from a peer
, sharedHeaderPeer :: !(TVar (Maybe PeerId))
-- ^ Peer currently syncing headers
, sharedMerklePeer :: !(TVar (Maybe PeerId))
-- ^ Peer currently downloading merkle blocks
, sharedSyncLock :: !Lock
-- ^ Lock on the header syncing process
, sharedBestHeader :: !(TVar NodeBlock)
-- ^ Our best block header
, sharedBestBlock :: !(TVar NodeBlock)
-- ^ Our best merkle block's height
, sharedTxGetData :: !(TVar (M.Map TxHash [(PeerId, PeerHost)]))
-- ^ List of Tx GetData requests
, sharedBloomFilter :: !(TVar (Maybe (BloomFilter, Int)))
-- ^ Bloom filter
, sharedTickleChan :: !(TBMChan (PeerId, PeerHost, BlockHash))
-- ^ Channel containing all the block tickles received from peers
, sharedTxChan :: !(TBMChan (PeerId, PeerHost, Tx))
-- ^ Transaction channel
, sharedRescan :: !(TMVar (Either Timestamp BlockHeight))
-- ^ Rescan requests from a timestamp or from a block height
, sharedMempool :: !(TVar Bool)
-- ^ Did we do a Mempool sync ?
, sharedSqlBackend :: !(Either SqlBackend ConnectionPool)
}
{- Peer Data -}
type PingNonce = Word64
-- Data stored about a peer
data PeerSession = PeerSession
{ peerSessionConnected :: !Bool
-- ^ True if the peer is connected (completed the handshake)
, peerSessionVersion :: !(Maybe Version)
-- ^ Contains the version message that we received from the peer
, peerSessionHeight :: !BlockHeight
-- ^ Current known height of the peer
, peerSessionChan :: !(TBMChan Message)
-- ^ Message channel to send messages to the peer
, peerSessionHost :: !PeerHost
-- ^ Host to which this peer is connected
, peerSessionThreadId :: !ThreadId
-- ^ Peer ThreadId
, peerSessionMerkleChan :: !(TBMChan (Either (MerkleBlock, MerkleTxs) Tx))
-- ^ Merkle block/Merkle transaction channel
, peerSessionPings :: !(TVar [PingNonce])
-- ^ Time at which we requested pings
, peerSessionScore :: !(Maybe NominalDiffTime)
-- ^ Ping scores for this peer (round trip times)
}
instance NFData PeerSession where
rnf PeerSession{..} =
rnf peerSessionConnected `seq`
rnf peerSessionVersion `seq`
rnf peerSessionHeight `seq`
peerSessionChan `seq`
rnf peerSessionHost `seq`
peerSessionThreadId `seq` ()
{- Peer Hosts -}
data PeerHost = PeerHost
{ peerHost :: !String
, peerPort :: !Int
}
deriving (Eq, Ord)
$(deriveJSON (dropFieldLabel 4) ''PeerHost)
peerHostString :: PeerHost -> String
peerHostString PeerHost{..} = concat [ peerHost, ":", show peerPort ]
instance NFData PeerHost where
rnf PeerHost{..} =
rnf peerHost `seq`
rnf peerPort
{- Node Status -}
data PeerStatus = PeerStatus
-- Regular fields
{ peerStatusPeerId :: !Int
, peerStatusHost :: !PeerHost
, peerStatusConnected :: !Bool
, peerStatusHeight :: !BlockHeight
, peerStatusProtocol :: !(Maybe Word32)
, peerStatusUserAgent :: !(Maybe String)
, peerStatusPing :: !(Maybe String)
, peerStatusDoSScore :: !(Maybe PeerHostScore)
-- Debug fields
, peerStatusHaveMerkles :: !Bool
, peerStatusHaveMessage :: !Bool
, peerStatusPingNonces :: ![PingNonce]
, peerStatusReconnectTimer :: !(Maybe Int)
, peerStatusLog :: !(Maybe [String])
}
$(deriveJSON (dropFieldLabel 10) ''PeerStatus)
data NodeStatus = NodeStatus
-- Regular fields
{ nodeStatusPeers :: ![PeerStatus]
, nodeStatusNetworkHeight :: !BlockHeight
, nodeStatusBestHeader :: !BlockHash
, nodeStatusBestHeaderHeight :: !BlockHeight
, nodeStatusBestBlock :: !BlockHash
, nodeStatusBestBlockHeight :: !BlockHeight
, nodeStatusBloomSize :: !Int
-- Debug fields
, nodeStatusHeaderPeer :: !(Maybe Int)
, nodeStatusMerklePeer :: !(Maybe Int)
, nodeStatusHaveHeaders :: !Bool
, nodeStatusHaveTickles :: !Bool
, nodeStatusHaveTxs :: !Bool
, nodeStatusGetData :: ![TxHash]
, nodeStatusRescan :: !(Maybe (Either Timestamp BlockHeight))
, nodeStatusMempool :: !Bool
, nodeStatusSyncLock :: !Bool
}
$(deriveJSON (dropFieldLabel 10) ''NodeStatus)
{- Getters / Setters -}
tryGetPeerSession :: PeerId -> NodeT STM (Maybe PeerSession)
tryGetPeerSession pid = do
peerMap <- readTVarS sharedPeerMap
case M.lookup pid peerMap of
Just sessTVar -> fmap Just $ lift $ readTVar sessTVar
_ -> return Nothing
getPeerSession :: PeerId -> NodeT STM PeerSession
getPeerSession pid = do
sessM <- tryGetPeerSession pid
case sessM of
Just sess -> return sess
_ -> throw $ NodeExceptionInvalidPeer $ ShowPeerId pid
newPeerSession :: PeerId -> PeerSession -> NodeT STM ()
newPeerSession pid sess = do
peerMapTVar <- asks sharedPeerMap
peerMap <- lift $ readTVar peerMapTVar
case M.lookup pid peerMap of
Just _ -> return ()
Nothing -> do
sessTVar <- lift $ newTVar sess
let newMap = M.insert pid sessTVar peerMap
lift $ writeTVar peerMapTVar $! newMap
modifyPeerSession :: PeerId -> (PeerSession -> PeerSession) -> NodeT STM ()
modifyPeerSession pid f = do
peerMap <- readTVarS sharedPeerMap
case M.lookup pid peerMap of
Just sessTVar -> lift $ modifyTVar' sessTVar f
_ -> return ()
removePeerSession :: PeerId -> NodeT STM (Maybe PeerSession)
removePeerSession pid = do
peerMapTVar <- asks sharedPeerMap
peerMap <- lift $ readTVar peerMapTVar
-- Close the peer TBMChan
sessM <- case M.lookup pid peerMap of
Just sessTVar -> lift $ do
sess@PeerSession{..} <- readTVar sessTVar
closeTBMChan peerSessionChan
return $ Just sess
_ -> return Nothing
-- Remove the peer from the peerMap
let newMap = M.delete pid peerMap
lift $ writeTVar peerMapTVar $! newMap
return sessM
getHostSession :: PeerHost
-> NodeT STM (Maybe PeerHostSession)
getHostSession ph = do
hostMap <- readTVarS sharedHostMap
lift $ case M.lookup ph hostMap of
Just hostSessionTVar -> Just <$> readTVar hostSessionTVar
_ -> return Nothing
modifyHostSession :: PeerHost
-> (PeerHostSession -> PeerHostSession)
-> NodeT STM ()
modifyHostSession ph f = do
hostMap <- readTVarS sharedHostMap
case M.lookup ph hostMap of
Just hostSessionTVar -> lift $ modifyTVar' hostSessionTVar f
_ -> newHostSession ph $!
f PeerHostSession { peerHostSessionScore = 0
, peerHostSessionReconnect = 1
, peerHostSessionLog = []
}
newHostSession :: PeerHost -> PeerHostSession -> NodeT STM ()
newHostSession ph session = do
hostMapTVar <- asks sharedHostMap
hostMap <- lift $ readTVar hostMapTVar
case M.lookup ph hostMap of
Just _ -> return ()
Nothing -> lift $ do
hostSessionTVar <- newTVar session
let newHostMap = M.insert ph hostSessionTVar hostMap
writeTVar hostMapTVar $! newHostMap
{- Host DOS Scores -}
bannedScore :: PeerHostScore
bannedScore = 100
minorDoS :: PeerHostScore -> PeerHostScore
minorDoS = (+ 1)
moderateDoS :: PeerHostScore -> PeerHostScore
moderateDoS = (+ 10)
severeDoS :: PeerHostScore -> PeerHostScore
severeDoS = (+ bannedScore)
isHostScoreBanned :: PeerHostScore -> Bool
isHostScoreBanned = (>= bannedScore)
{- STM Utilities -}
orElseNodeT :: NodeT STM a -> NodeT STM a -> NodeT STM a
orElseNodeT a b = do
s <- ask
lift $ runNodeT a s `orElse` runNodeT b s
{- TVar Utilities -}
readTVarS :: (SharedNodeState -> TVar a) -> NodeT STM a
readTVarS = lift . readTVar <=< asks
writeTVarS :: (SharedNodeState -> TVar a) -> a -> NodeT STM ()
writeTVarS f val = lift . flip writeTVar val =<< asks f
{- TMVar Utilities -}
takeTMVarS :: (SharedNodeState -> TMVar a) -> NodeT STM a
takeTMVarS = lift . takeTMVar <=< asks
readTMVarS :: (SharedNodeState -> TMVar a) -> NodeT STM a
readTMVarS = lift . readTMVar <=< asks
tryReadTMVarS :: (SharedNodeState -> TMVar a) -> NodeT STM (Maybe a)
tryReadTMVarS = lift . tryReadTMVar <=< asks
putTMVarS :: (SharedNodeState -> TMVar a) -> a -> NodeT STM ()
putTMVarS f val = lift . flip putTMVar val =<< asks f
tryPutTMVarS :: (SharedNodeState -> TMVar a) -> a -> NodeT STM Bool
tryPutTMVarS f val = lift . flip tryPutTMVar val =<< asks f
swapTMVarS :: (SharedNodeState -> TMVar a) -> a -> NodeT STM ()
swapTMVarS f val = lift . flip putTMVar val =<< asks f
isEmptyTMVarS :: (SharedNodeState -> TMVar a) -> NodeT STM Bool
isEmptyTMVarS f = lift . isEmptyTMVar =<< asks f
data NodeException
= NodeExceptionBanned
| NodeExceptionConnected
| NodeExceptionInvalidPeer !ShowPeerId
| NodeExceptionPeerNotConnected !ShowPeerId
| NodeException !String
deriving (Show, Typeable)
instance Exception NodeException
isNodeException :: SomeException -> Bool
isNodeException se = isJust (fromException se :: Maybe NodeException)
catchAny :: MonadBaseControl IO m
=> m a -> (SomeException -> m a) -> m a
catchAny = catch
catchAny_ :: MonadBaseControl IO m
=> m () -> m ()
catchAny_ = flip catchAny $ \_ -> return ()

View File

@ -1,16 +0,0 @@
module Main where
import Test.Framework (defaultMain)
import Network.Haskoin.Constants
import qualified Network.Haskoin.Node.Tests (tests)
import qualified Network.Haskoin.Node.Units (tests)
main :: IO ()
main = do
setProdnet
defaultMain
( Network.Haskoin.Node.Tests.tests
++ Network.Haskoin.Node.Units.tests
)

View File

@ -1,12 +0,0 @@
module Network.Haskoin.Node.Tests (tests) where
import Test.Framework (Test, testGroup)
-- import Test.Framework.Providers.QuickCheck2 (testProperty)
tests :: [Test]
tests =
[ testGroup "Serialize & de-serialize haskoin node types to JSON"
[
]
]

View File

@ -1,252 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module Network.Haskoin.Node.Units where
import Control.Monad (forM_, when)
import Control.Monad.Logger (NoLoggingT)
import Control.Monad.Trans (MonadIO, liftIO)
import Control.Monad.Trans.Resource (ResourceT)
import Data.Maybe (fromJust, isNothing,
maybeToList)
import Data.Word (Word32)
import Database.Persist.Sqlite (SqlPersistT,
runMigrationSilent, runSqlite)
import Network.Haskoin.Block
import Network.Haskoin.Constants
import Network.Haskoin.Node.HeaderTree
import Test.Framework (Test, testGroup)
import Test.Framework.Providers.HUnit (testCase)
import Test.HUnit (Assertion, assertBool,
assertEqual, assertFailure)
-- TODO: Make sure that evalNewChain for a partially overlapping best chain
-- properly evaluates to BestChain.
type App = SqlPersistT (NoLoggingT (ResourceT IO))
tests :: [Test]
tests =
[ testGroup "Header Tree"
[ testCase "Initalization successful" $ runUnit initialize
, testCase "Add second block" $ runUnit addSecondBlock
, testCase "Blockchain head correct" $ runUnit blockChainHead
, testCase "Find fork node" $ runUnit forkNode
, testCase "Find fork node (non-head)" $ runUnit forkNodeNonHead
, testCase "Find fork node (same chain)" $ runUnit forkNodeSameChain
, testCase "Get best chain" $ runUnit getBestChain
, testCase "Get side chain" $ runUnit getSideChain
, testCase "Nodes at height" $ runUnit getNodesHeight
, testCase "Block locator to head" $ runUnit bLocatorToHead
, testCase "Block locator to non-head" $ runUnit bLocatorToNode
, testCase "Find split node" $ runUnit splitNode
]
]
initialize :: App ()
initialize = do
initHeaderTree
bM <- getBlockByHash (headerHash genesisHeader)
liftIO $ assertEqual "Genesis node in header tree" (Just nGenesisBlock) bM
hs <- getHeads
liftIO $ assertEqual "Genesis node is only head" [nGenesisBlock] hs
bh <- getBestBlock
liftIO $ assertEqual "Genesis node matches best header" nGenesisBlock bh
addSecondBlock :: App ()
addSecondBlock = do
initHeaderTree
let block = head chain0
liftIO $ assertEqual "Block builds on genesis block"
(headerHash genesisHeader)
(nodePrev block)
putBlock block
block' <- getBlockByHash $ nodeHash block
liftIO $ assertEqual "Block can be retrieved" (Just block) block'
blockChainHead :: App ()
blockChainHead = mockBlockChain >> do
heads <- getHeads
liftIO $ assertEqual "Heads match"
[last chain0, last chain1, last chain2, last chain3]
heads
bh <- getBestBlock
liftIO $ assertEqual "Best block has correct hash"
(nodeHash $ last chain3) (nodeHash bh)
liftIO $ assertEqual "Best block height is right"
(nodeBlockHeight $ last chain3) (nodeBlockHeight bh)
forkNode :: App ()
forkNode = mockBlockChain >> do
let l = last chain2
r = last chain3
bn <- splitBlock l r
liftIO $ assertEqual "Split block are correct"
(chain0 !! 1) bn
commonLM <- getBlockByHeight l $ nodeBlockHeight bn
when (isNothing commonLM) $ liftIO $
assertFailure "Could not find fork on left side"
let commonL = fromJust commonLM
commonRM <- getBlockByHeight r $ nodeBlockHeight bn
when (isNothing commonRM) $ liftIO $
assertFailure "Could not find fork on right side"
let commonR = fromJust commonRM
firstLM <- getBlockByHeight l (nodeBlockHeight bn + 1)
when (isNothing firstLM) $ liftIO $
assertFailure "Could not find fork child on left side"
let firstL = fromJust firstLM
firstRM <- getBlockByHeight r (nodeBlockHeight bn + 1)
when (isNothing firstLM) $ liftIO $
assertFailure "Could not find fork child on right side"
let firstR = fromJust firstRM
liftIO $ assertEqual "Fork node is same in both sides" commonL commonR
liftIO $ assertEqual "Fork node connect with left side"
(nodeHash commonL)
(nodePrev firstL)
liftIO $ assertEqual "Fork node connect with right side"
(nodeHash commonR)
(nodePrev firstR)
liftIO $ assertBool "After-fork chains diverge" $ firstL /= firstR
liftIO $ assertEqual "Fork node matches hardcoded one"
(chain0 !! 1) commonL
forkNodeNonHead :: App ()
forkNodeNonHead = mockBlockChain >> do
let l = chain2 !! 1
r = chain1 !! 1
height <- nodeBlockHeight <$> splitBlock l r
splitM <- getBlockByHeight l height
liftIO $ assertEqual "Fork node is correct" (Just $ chain1 !! 1) splitM
forkNodeSameChain :: App ()
forkNodeSameChain = mockBlockChain >> do
let l = chain3 !! 5
r = chain3 !! 3
height <- nodeBlockHeight <$> splitBlock l r
splitM <- getBlockByHeight r height
liftIO $ assertEqual "Fork node is correct" (Just $ chain3 !! 3) splitM
getBestChain :: App ()
getBestChain = mockBlockChain >> do
h <- getBestBlock
ch <- getBlocksFromHeight h 0 0
liftIO $ assertEqual "Best chain correct" bch ch
where
bch = nGenesisBlock : take 2 chain0 ++ chain3
getSideChain :: App ()
getSideChain = mockBlockChain >> do
ch <- getBlocksFromHeight (chain2 !! 1) 0 0
liftIO $ assertEqual "Side chain correct" sch ch
where
sch = nGenesisBlock :
take 3 chain0 ++ take 2 chain1 ++ take 2 chain2
getNodesHeight :: App ()
getNodesHeight = mockBlockChain >> do
ns <- getBlocksAtHeight 3
liftIO $ assertEqual "Nodes at height match" hns ns
where
hns = [chain0 !! 2, head chain3]
bLocatorToHead :: App ()
bLocatorToHead = do
mockBlockChain
putBlocks bs
h <- getBestBlock
liftIO $ assertEqual "Head matches" (last bs) h
ls <- bLocator h
liftIO $ assertEqual "Last is genesis"
(last ls)
(headerHash genesisHeader)
liftIO $ assertEqual "First is current head"
(head ls)
(nodeHash h)
last10 <- map nodeHash . reverse <$>
getBlocksFromHeight h 0 (nodeBlockHeight h - 9)
liftIO $ assertEqual "Last ten blocks contiguous"
last10
(take 10 ls)
let h10 = nodeBlockHeight h - 10
bhs <- map (nodeHash . fromJust) <$>
mapM (getBlockByHeight h)
[h10, h10 - 2, h10 - 6, h10 - 14, h10 - 30, h10 - 62]
liftIO $ assertEqual "All block hashes correct"
(last10 ++ bhs ++ [headerHash genesisHeader])
ls
where
bs = manyBlocks $ last chain1
bLocatorToNode :: App ()
bLocatorToNode = do
mockBlockChain
putBlocks bs
n <- fromJust <$> getBlockByHash (nodeHash $ chain3 !! 4)
ls <- bLocator n
xs <- map nodeHash . reverse <$>
getBlocksFromHeight n 0 0
liftIO $ assertEqual "Block locator for non-head node is correct" xs ls
where
bs = manyBlocks $ last chain1
splitNode :: App ()
splitNode = do
mockBlockChain
(split, ls, rs) <- splitChains (last chain2, 0) (last chain3, 0)
liftIO $ assertEqual "Split node correct" (chain0 !! 1) split
liftIO $ assertEqual "Left correct"
([chain0 !! 2] ++ take 2 chain1 ++ chain2)
ls
liftIO $ assertEqual "Right correct" chain3 rs
runUnit :: App () -> Assertion
runUnit action = runSqlite ":memory:" $ do
_ <- runMigrationSilent migrateHeaderTree
action
mockBlockChain :: MonadIO m => SqlPersistT m ()
mockBlockChain = do
initHeaderTree
forM_ (concat [chain0, chain1, chain2, chain3]) putBlock
manyBlocks :: NodeBlock -> [NodeBlock]
manyBlocks b =
tail $ reverse $ foldBlock (Just b) $ zip [18..117] (repeat 4)
chain0 :: [NodeBlock]
chain0 =
tail $ reverse $ foldBlock Nothing $ zip [1..4] (repeat 0)
chain1 :: [NodeBlock]
chain1 =
tail $ reverse $ foldBlock (Just $ chain0 !! 2) $ zip [5..7] (repeat 1)
chain2 :: [NodeBlock]
chain2 =
tail $ reverse $ foldBlock (Just $ chain1 !! 1) $ zip [8..10] (repeat 2)
chain3 :: [NodeBlock]
chain3 =
tail $ reverse $ foldBlock (Just $ chain0 !! 1) $ zip [11..17] (repeat 3)
foldBlock :: Maybe NodeBlock -> [(Word32, Word32)] -> [NodeBlock]
foldBlock nM =
foldl f (maybeToList nM)
where
f [] _ = [nGenesisBlock]
f ls@(l:_) (n, chain) = mockBlock l chain n : ls
mockBlock :: NodeBlock -> Word32 -> Word32 -> NodeBlock
mockBlock parent chain n = nodeBlock parent chain bh
where
bh = BlockHeader
(blockVersion $ nHeader parent)
(nodeHash parent)
"0000000000000000000000000000000000000000000000000000000000000000"
(nodeTimestamp parent + 600)
(blockBits $ nHeader parent)
n

View File

@ -1,169 +0,0 @@
# stylish-haskell configuration file
# ==================================
# The stylish-haskell tool is mainly configured by specifying steps. These steps
# are a list, so they have an order, and one specific step may appear more than
# once (if needed). Each file is processed by these steps in the given order.
steps:
# Convert some ASCII sequences to their Unicode equivalents. This is disabled
# by default.
# - unicode_syntax:
# # In order to make this work, we also need to insert the UnicodeSyntax
# # language pragma. If this flag is set to true, we insert it when it's
# # not already present. You may want to disable it if you configure
# # language extensions using some other method than pragmas. Default:
# # true.
# add_language_pragma: true
# Align the right hand side of some elements. This is quite conservative
# and only applies to statements where each element occupies a single
# line.
- simple_align:
cases: true
top_level_patterns: true
records: true
# Import cleanup
- imports:
# There are different ways we can align names and lists.
#
# - global: Align the import names and import list throughout the entire
# file.
#
# - file: Like global, but don't add padding when there are no qualified
# imports in the file.
#
# - group: Only align the imports per group (a group is formed by adjacent
# import lines).
#
# - none: Do not perform any alignment.
#
# Default: global.
align: global
# Folowing options affect only import list alignment.
#
# List align has following options:
#
# - after_alias: Import list is aligned with end of import including
# 'as' and 'hiding' keywords.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - with_alias: Import list is aligned with start of alias or hiding.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - new_line: Import list starts always on new line.
#
# > import qualified Data.List as List
# > (concat, foldl, foldr, head, init, last, length)
#
# Default: after alias
list_align: after_alias
# Long list align style takes effect when import is too long. This is
# determined by 'columns' setting.
#
# - inline: This option will put as much specs on same line as possible.
#
# - new_line: Import list will start on new line.
#
# - new_line_multiline: Import list will start on new line when it's
# short enough to fit to single line. Otherwise it'll be multiline.
#
# - multiline: One line per import list entry.
# Type with contructor list acts like single import.
#
# > import qualified Data.Map as M
# > ( empty
# > , singleton
# > , ...
# > , delete
# > )
#
# Default: inline
long_list_align: inline
# List padding determines indentation of import list on lines after import.
# This option affects 'list_align' and 'long_list_align'.
list_padding: 4
# Separate lists option affects formating of import list for type
# or class. The only difference is single space between type and list
# of constructors, selectors and class functions.
#
# - true: There is single space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable (fold, foldl, foldMap))
#
# - false: There is no space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable(fold, foldl, foldMap))
#
# Default: true
separate_lists: true
# Language pragmas
- language_pragmas:
# We can generate different styles of language pragma lists.
#
# - vertical: Vertical-spaced language pragmas, one per line.
#
# - compact: A more compact style.
#
# - compact_line: Similar to compact, but wrap each line with
# `{-#LANGUAGE #-}'.
#
# Default: vertical.
style: vertical
# Align affects alignment of closing pragma brackets.
#
# - true: Brackets are aligned in same collumn.
#
# - false: Brackets are not aligned together. There is only one space
# between actual import and closing bracket.
#
# Default: true
align: true
# stylish-haskell can detect redundancy of some language pragmas. If this
# is set to true, it will remove those redundant pragmas. Default: true.
remove_redundant: true
# Replace tabs by spaces. This is disabled by default.
# - tabs:
# # Number of spaces to use for each tab. Default: 8, as specified by the
# # Haskell report.
# spaces: 8
# Remove trailing whitespace
- trailing_whitespace: {}
# A common setting is the number of columns (parts of) code will be wrapped
# to. Different steps take this into account. Default: 80.
columns: 80
# Sometimes, language extensions are specified in a cabal file or from the
# command line instead of using language pragmas in the file. stylish-haskell
# needs to be aware of these, so it can parse the file correctly.
#
# No language extensions are enabled by default.
language_extensions:
- DeriveDataTypeable
- EmptyDataDecls
- FlexibleContexts
- FlexibleInstances
- GADTs
- GeneralizedNewtypeDeriving
- MultiParamTypeClasses
- OverloadedStrings
- QuasiQuotes
- RecordWildCards
- TemplateHaskell
- TypeFamilies

View File

@ -1,27 +0,0 @@
FROM ubuntu:14.04
MAINTAINER Jean-Pierre Rupp <root@haskoin.com>
RUN apt-get update
RUN apt-get install -y \
git \
wget \
libleveldb-dev \
libzmq3-dev \
libsnappy-dev \
pkg-config \
zlib1g-dev
RUN wget -q -O- \
https://s3.amazonaws.com/download.fpcomplete.com/ubuntu/fpco.key \
| apt-key add -
RUN echo 'deb http://download.fpcomplete.com/ubuntu/trusty stable main' \
> /etc/apt/sources.list.d/fpco.list
RUN apt-get update && apt-get install -y stack
WORKDIR /usr/local/src
RUN git clone https://github.com/haskoin/haskoin-wallet.git
WORKDIR /usr/local/src/haskoin-wallet
RUN stack setup
RUN stack install
RUN mv /root/.local/bin/hw /usr/local/bin
RUN rm -rf /root/.stack /root/.local
WORKDIR /root
CMD [ "/usr/local/bin/hw" ]

View File

@ -1,2 +0,0 @@
import Distribution.Simple
main = defaultMain

View File

@ -1,24 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@ -1,7 +0,0 @@
module Main where
import Network.Haskoin.Wallet.Client
main :: IO ()
main = clientMain

View File

@ -1,123 +0,0 @@
# ZeroMQ socket on which to listen to. Either absolute path or relative to
# work-dir/network.
bind-socket: ipc://hw.sock
bind-socket-notif: ipc://notif.sock
# ZeroMQ socket to communicate with the server.
# Either absolute path or relative to the server work-dir/network path.
connect-uri: ipc://hw.sock
connect-uri-notif: ipc://notif.sock
# Server mode. Can be either online or offline. In offline mode, the SPV
# daemon does not start and only the local wallet is available to query.
server-mode: online
# False positive rate for the bloom filters.
bloom-false-positive: 0.00001
# Database connection information.
database:
testnet5:
database: hw-wallet.sqlite3
poolsize: 1
prodnet:
database: hw-wallet.sqlite3
poolsize: 1
# List of trusted bitcoin full-nodes to connect to.
bitcoin-full-nodes:
testnet5:
- host: seed.testnet5.b-pay.net
port: 18555
- host: bitcoin-testnet.bloqseeds.net
port: 18555
prodnet:
- host: seed.mainnet.b-pay.net
port: 8333
- host: seed.ob1.io
port: 8333
- host: seed.blockchain.info
port: 8333
- host: bitcoin.bloqseeds.net
port: 8333
- host: seed.bitcoin.sipa.be
port: 8333
- host: dnsseed.bluematt.me
port: 8333
- host: dnsseed.bitcoin.dashjr.org
port: 8333
- host: seed.bitcoinstats.com
port: 8333
- host: seed.bitcoin.jonasschnelli.ch
port: 8333
# Log file name. Either absolute path or relative to work-dir/network
log-file: hw.log
# PID file name. Either absolute path or relative to work-dir/network.
pid-file: hw.pid
# Compile time configuration value. Either absolute path or relative to
# work-dir. Can only be set as environment variable.
config-file: config.yml
# Default output size for commands such as page sizes.
output-size: 10
# Type of addresses to display. Example: external, internal
address-type: external
# Displaz public keys instead of addresses in relevant commands
display-pubkeys: false
# Use reverse paging for diplaying addresses and txs when set to True.
reverse-paging: false
# Sign new and imported transactions.
sign-transactions: true
# Default fee to pay (in satoshi) for every 1000 bytes.
transaction-fee: 10000
# Minimum number of confirmations for spending coins and displaying balances.
minimum-confirmations: 0
# Display the balance including offline transactions
offline: false
# Entropy in bytes to use when generating a mnemonic (between 16 and 32)
seed-entropy: 16
# How command-line output should be displayed. Supported values are:
# normal, json or yaml.
display-format: normal
# Detach the SPV server from the terminal when launched
detach-server: false
# Use Testnet3
use-testnet: false
# Haskoin working directory. Either absolute path or relative to users home.
# Defaults to an appropriate OS-specific value.
work-dir: ""
# Log level. Valid values are debug, info, warn and error.
log-level: info
# Print verbose
verbose: false
# Recipient pays transaction fee. DANGEROUS.
recipient-fee: false
# Server key for authentication and encryption (server config).
server-key:
# Server public key for authentication and encryption (client config).
server-key-public:
# Client key for authentication and encryption (client config).
client-key:
# Client public key for aunthentication and encryption (client + server config).
client-key-public:

View File

@ -1,53 +0,0 @@
Server commands:
start [--detach] Start the haskoin daemon
stop Stop the haskoin daemon
status [--verbose] Display node runtime information
Account commands:
newacc name Create a new account
newread name Create a new read-only account
newms name M N Create a new multisig account
newreadms name M N Create a new read-only ms account
addkey acc Add pubkeys to a multisig account
setgap acc gap Set the address gap for an account
accounts [page] [-c pagesize] [-r] List all accounts in a keyring
account acc Display an account by name
rename old new Rename account
Address commands:
list acc [page] [-c pagesize] [-r] Display addresses by page
unused acc [page] [-c pagesize] [-r] Display unused addresses
label acc index label Set the label of an address
addrtxs acc index [page] [-c pagesize] Display txs related to an address
getindex acc key Get key index by pubkey
genaddrs acc index [--internal] Generate addresses up to this index
uri acc index [amount] [msg] Create a Bitcoin payment request URI
Transaction commands:
txs acc [page] [-c pagesize] [-r] Display all transactions by page
pending acc [page] [-c pagesize] [-r] Display pending transactions by page
dead acc [page] [-c pagesize] [-r] Display dead transactions by page
send acc addr amount [-S] Send coins to an address
sendmany acc {addr:amount...} [-S] Send coins to many addresses
import acc Import a transaction (does not sign)
sign acc txid Sign one of your offline transactions
balance acc [--minconf] [--offline] Display account balance
gettx acc txid Get a transaction by txid
deletetx txid Delete unconfirmed transaction
sync acc {hash|height} [page] Get blocks following specified one
Offline tx signing commands:
getoffline acc txhash Get data to sign a tx offline
signoffline acc Sign a tx with offline signing data
Utility commands:
decodetx Decode HEX transaction
rescan [timestamp] Rescan the wallet
keypair Get curve key pair for ØMQ auth
monitor [account] Monitor events (no account: blocks)
blockinfo [hash1] [hash2] [...] Get block header information by hash
dice dicerolls Mnemonic from 99 dice rolls (1 to 6)
Other commands:
version Display version information
help Display this help information

View File

@ -1,83 +0,0 @@
Account
name Text maxlen=200
type AccountType maxlen=64
master XPrvKey Maybe
keys [XPubKey]
gap Word32
created UTCTime
UniqueAccount name
deriving Show
WalletAddr
account AccountId
address Address maxlen=64
index KeyIndex
type AddressType maxlen=16
label Text
redeem ScriptOutput Maybe
key PubKeyC Maybe maxlen=66
created UTCTime
UniqueAddr account address
UniqueAddrRev address account
UniqueAddrIndex account type index
deriving Show
WalletTx
account AccountId
hash TxHash maxlen=64
nosigHash TxHash maxlen=64
type TxType maxlen=16
inValue Word64
outValue Word64
inputs [AddressInfo]
outputs [AddressInfo]
change [AddressInfo]
tx Tx
isCoinbase Bool
confidence TxConfidence maxlen=16
confirmedBy BlockHash Maybe maxlen=64
confirmedHeight BlockHeight Maybe
confirmedDate Timestamp Maybe
created UTCTime
UniqueAccTx account hash
UniqueAccTxRev hash account
UniqueAccNoSig account nosigHash
UniqueAccNoSigRev nosigHash account
deriving Show
WalletCoin
account AccountId
hash TxHash maxlen=64
pos Word32
tx WalletTxId
addr WalletAddrId
value Word64
script ScriptOutput
created UTCTime
UniqueCoin account hash pos
UniqueCoinRev hash pos account
UniqueCoinTx tx pos
UniqueCoinTxRev pos tx
deriving Show
SpentCoin
account AccountId
hash TxHash maxlen=64
pos Word32
spendingTx WalletTxId
created UTCTime
UniqueSpentCoins account hash pos
UniqueSpentCoinsRev hash pos account
UniqueSpentTx spendingTx hash pos
UniqueSpentTxRev hash pos spendingTx
deriving Show
WalletState
height BlockHeight
block BlockHash maxlen=64
bloomFilter BloomFilter
bloomElems Int
bloomFp Double
version Int
created UTCTime
deriving Show

View File

@ -1,187 +0,0 @@
module Main where
import Network.Haskoin.Wallet (Config(..),
WalletRequest(..), WalletResponse(..),
AddressType(..), OutputFormat(..),
SPVMode(..), NodeAction(..))
import Network.Haskoin.Wallet.Server (runSPVServerWithContext)
import Network.Haskoin.Wallet.Internals (BTCNode(..), Notif(..))
import qualified Network.Haskoin.Node.STM as Node
import Data.String.Conversions (cs)
import qualified System.ZMQ4 as ZMQ
import qualified Control.Monad.Logger as Log
import qualified Data.HashMap.Strict as HM
import qualified Database.Persist.Sqlite as DB
import qualified Control.Monad.Trans.Resource as Resource
import qualified Data.Aeson as JSON
import qualified Control.Concurrent as Con
import qualified Data.Aeson.Encode.Pretty as PrettyJSON
import qualified Control.Monad as M
import qualified Control.Exception as Except
databaseConf :: DB.SqliteConf
databaseConf = DB.SqliteConf "/tmp/tmpdb" 1
cmdSocket :: String
cmdSocket = "inproc://cmd"
notifSocket :: String
notifSocket = "inproc://notif"
-- |Simple example app that embeds a haskoin-wallet server.
-- Start wallet server + notification thread, and execute Status command when pressing ENTER
main :: IO ()
main = ZMQ.withContext $ \ctx -> do
-- Server
putStrLn "Starting server..."
_ <- Con.forkIO $ runWallet walletServerConf ctx
-- Notify thread
putStrLn "Starting notification thread..."
_ <- Con.forkIO $ notifyThread ctx notifyHandler
-- Status loop
M.forever $ do
putStrLn "Press ENTER to get server status..."
_ <- getLine
cmdGetStatus ctx >>= printStatusJSON
where
printStatusJSON = putStrLn . cs . PrettyJSON.encodePretty
notifyHandler notif =
putStrLn $ "NOTIFY: New block: " ++ cs (PrettyJSON.encodePretty notif)
-- |Run haskoin-wallet using the specified ZeroMQ Context,
-- and log to stderr.
runWallet :: Config -> ZMQ.Context -> IO ()
runWallet cfg ctx = run $ runSPVServerWithContext cfg ctx
where run = Resource.runResourceT . runLogging
runLogging = Log.runStderrLoggingT . Log.filterLogger logFilter
logFilter _ l = l >= configLogLevel cfg
cmdGetStatus :: ZMQ.Context -> IO Node.NodeStatus
cmdGetStatus ctx =
sendCmdOrFail (NodeActionReq NodeActionStatus) ctx >>=
\res -> case res of
Nothing -> error "ERROR: Status command: no response."
Just status -> return status
sendCmdOrFail :: (JSON.FromJSON a, JSON.ToJSON a)
=> WalletRequest
-> ZMQ.Context
-> IO (Maybe a)
sendCmdOrFail cmd ctx =
sendCmd cmd ctx >>=
either error return >>=
\res -> case res of
ResponseError e -> error $ "ERROR: Send cmd, ResponseError: " ++ cs e
ResponseValid r -> return r
sendCmd :: (JSON.FromJSON a, JSON.ToJSON a)
=> WalletRequest
-> ZMQ.Context
-> IO (Either String (WalletResponse a))
sendCmd req ctx =
ZMQ.withSocket ctx ZMQ.Req $ \sock -> do
ZMQ.setLinger (ZMQ.restrict (0 :: Int)) sock
ZMQ.connect sock cmdSocket
ZMQ.send sock [] (cs $ JSON.encode req)
JSON.eitherDecode . cs <$> ZMQ.receive sock
-- |Connect to notify socket, subscribe to new blocks,
-- and execute the supplied handler for each new block as it arrives.
notifyThread :: ZMQ.Context -> (Notif -> IO ()) -> IO ()
notifyThread ctx handler = waitAndCatch $
ZMQ.withSocket ctx ZMQ.Sub $ \sock -> do
ZMQ.setLinger (ZMQ.restrict (0 :: Int)) sock
ZMQ.connect sock notifSocket
ZMQ.subscribe sock "[block]"
putStrLn "NOTIFY: Connected. Subscribed to new blocks."
M.forever $ do
[_,m] <- ZMQ.receiveMulti sock
notif <- either failOnErr return $ JSON.eitherDecode (cs m)
handler notif
where
failOnErr = fail . ("NOTIFY: ERROR: recv failed: " ++)
waitAndCatch ioa = Con.threadDelay 10000 >> ioa `Except.finally` waitAndCatch ioa
btcNodes :: [BTCNode]
btcNodes =
[ BTCNode "dnsseed.bluematt.me" 8333
, BTCNode "dnsseed.bitcoin.dashjr.org" 8333
, BTCNode "dnsseed.bluematt.me" 8333
, BTCNode "seed.bitcoinstats.com" 8333
, BTCNode "seed.bitcoin.jonasschnelli.ch" 8333
, BTCNode "seed.bitcoin.sipa.be" 8333
, BTCNode "seed.bitnodes.io" 8333
, BTCNode "seed.btcc.com" 8333
]
walletServerConf :: Config
walletServerConf = Config
{ configCount = 100
-- ^ Output size of commands
, configMinConf = 6
-- ^ Minimum number of confirmations
, configSignTx = True
-- ^ Sign transactions
, configFee = 50000
-- ^ Fee to pay per 1000 bytes when creating new transactions
, configRcptFee = False
-- ^ Recipient pays fee (dangerous, no config file setting)
, configAddrType = AddressExternal
-- ^ Return internal instead of external addresses
, configDisplayPubKeys = False
-- ^ Display public keys instead of addresses
, configOffline = False
-- ^ Display the balance including offline transactions
, configEntropy = 16
-- ^ Entropy in bytes to use when generating a mnemonic (between 16 and 32)
, configReversePaging = False
-- ^ Use reverse paging for displaying addresses and transactions
, configDerivIndex = 0
-- ^ Derivation path when creating account
, configFormat = OutputNormal
-- ^ How to format the command-line results
, configConnect = cmdSocket
-- ^ ZeroMQ socket to connect to (location of the server)
, configConnectNotif = notifSocket
-- ^ ZeroMQ socket to connect for notifications
, configDetach = False
-- ^ Detach server when launched from command-line
, configFile = ""
-- ^ Configuration file
, configTestnet = False
-- ^ Use Testnet3 network
, configDir = ""
-- ^ Working directory
, configBind = cmdSocket
-- ^ Bind address for the ZeroMQ socket
, configBindNotif = notifSocket
-- ^ Bind address for ZeroMQ notifications
, configBTCNodes = HM.fromList [ ( "prodnet", btcNodes ) ]
-- ^ Trusted Bitcoin full nodes to connect to
, configMode = SPVOnline
-- ^ Operation mode of the SPV node.
, configBloomFP = 0.00001
-- ^ False positive rate for the bloom filter.
, configDatabase = HM.fromList [ ( "prodnet", databaseConf ) ]
-- ^ Database configuration
, configLogFile = ""
-- ^ Log file
, configPidFile = ""
-- ^ PID File
, configLogLevel = Log.LevelInfo
-- ^ Log level
, configVerbose = True
-- ^ Verbose
, configServerKey = Nothing
-- ^ Server key for authentication and encryption (server config)
, configServerKeyPub = Nothing
-- ^ Server public key for authentication and encryption (client config)
, configClientKey = Nothing
-- ^ Client key for authentication and encryption (client config)
, configClientKeyPub = Nothing
-- ^ Client public key for authentication and encryption
}

View File

@ -1,184 +0,0 @@
name: haskoin-wallet
version: 0.4.3
synopsis:
Implementation of a Bitcoin SPV Wallet with BIP32 and multisig support.
description:
This package provides a SPV (simple payment verification) wallet
implementation. It features BIP32 key management, deterministic signatures
(RFC-6979) and first order support for multi-signature transactions. You
can communicate with the wallet process through a ZeroMQ API or through a
command-line tool called "hw" which is also provided in this package.
homepage: http://github.com/haskoin/haskoin
bug-reports: http://github.com/haskoin/haskoin/issues
tested-with: GHC==8.0.2
license: PublicDomain
license-file: UNLICENSE
author: Philippe Laprade, Jean-Pierre Rupp
maintainer: xenog@protonmail.com
category: Bitcoin, Finance, Network
build-type: Simple
cabal-version: >= 1.9.2
extra-source-files: config/help, config/config.yml, config/models
source-repository head
type: git
location: git://github.com/haskoin/haskoin.git
Flag library-only
Description: Do not build the executables
Default: False
library
hs-source-dirs: src
exposed-modules: Network.Haskoin.Wallet
Network.Haskoin.Wallet.Model
Network.Haskoin.Wallet.Client
Network.Haskoin.Wallet.Server
Network.Haskoin.Wallet.Settings
Network.Haskoin.Wallet.Internals
other-modules: Network.Haskoin.Wallet.Types
Network.Haskoin.Wallet.Types.BlockInfo
Network.Haskoin.Wallet.Accounts
Network.Haskoin.Wallet.Transaction
Network.Haskoin.Wallet.Block
Network.Haskoin.Wallet.Server.Handler
Network.Haskoin.Wallet.Client.Commands
Network.Haskoin.Wallet.Client.PrettyJson
Network.Haskoin.Wallet.Database
extensions: TemplateHaskell
QuasiQuotes
OverloadedStrings
MultiParamTypeClasses
TypeFamilies
GADTs
FlexibleContexts
FlexibleInstances
EmptyDataDecls
DeriveDataTypeable
RecordWildCards
GeneralizedNewtypeDeriving
build-depends: aeson >= 1.2 && < 1.3
, aeson-pretty >= 0.7 && < 0.9
, base >= 4.8 && < 5
, base64-bytestring >= 1.0.0.1
, bytestring >= 0.10 && < 0.11
, cereal >= 0.5 && < 0.6
, containers >= 0.5 && < 0.6
, conduit >= 1.2 && < 1.3
, deepseq >= 1.4 && < 1.5
, data-default >= 0.5 && < 0.8
, directory >= 1.2 && < 1.4
, daemons >= 0.2 && < 0.3
, entropy >= 0.3.7 && < 0.4
, exceptions >= 0.6 && < 0.9
, esqueleto >= 2.4 && < 2.6
, file-embed >= 0.0 && < 0.1
, filepath >= 1.4 && < 1.5
, haskeline
, haskoin-core >= 0.3 && < 0.5
, haskoin-node >= 0.3 && < 0.5
, lifted-async >= 0.2 && < 0.10
, lifted-base >= 0.2 && < 0.3
, monad-logger >= 0.3.13 && < 0.4
, monad-control >= 1.0 && < 1.1
, mtl >= 2.1 && < 2.3
, persistent >= 2.7 && < 2.8
, persistent-template >= 2.1 && < 2.6
, persistent-sqlite >= 2.2 && < 2.7
, resourcet >= 1.1 && < 1.2
, semigroups
, split >= 0.2 && < 0.3
, stm >= 2.4 && < 2.5
, stm-chans >= 3.0 && < 3.1
, stm-conduit >= 2.6 && < 3.1
, string-conversions >= 0.4 && < 0.5
, text >= 0.11 && < 1.3
, time >= 1.8 && < 1.9
, transformers-base >= 0.4 && < 0.5
, unix >= 2.6 && < 2.8
, unordered-containers >= 0.2 && < 0.3
, uri-encode >= 1.5.0.5
, yaml >= 0.8 && < 0.9
, zeromq4-haskell >= 0.7 && < 0.8
ghc-options: -Wall
executable hw
if flag(library-only)
Buildable: False
main-is: Main.hs
build-depends: base, haskoin-wallet
hs-source-dirs: app
ghc-options: -Wall
-O3
-threaded
-eventlog
-rtsopts
-with-rtsopts=-N4
test-suite test-haskoin-wallet
type: exitcode-stdio-1.0
main-is: Main.hs
other-modules: Network.Haskoin.Wallet.Arbitrary
, Network.Haskoin.Wallet.Tests
, Network.Haskoin.Wallet.Units
extensions: RecordWildCards
OverloadedStrings
build-depends: aeson >= 1.2 && < 1.3
, base >= 4.8 && < 5
, bytestring >= 0.10 && < 0.11
, containers >= 0.5 && < 0.6
, directory >= 1.2 && < 1.4
, haskoin-core >= 0.3 && < 0.5
, haskoin-node >= 0.3 && < 0.5
, haskoin-wallet
, monad-logger >= 0.3 && < 0.4
, mtl >= 2.1 && < 2.3
, persistent >= 2.7 && < 2.8
, persistent-sqlite >= 2.2 && < 2.7
, resourcet >= 1.1 && < 1.2
, text >= 0.11 && < 1.3
, unordered-containers >= 0.2 && < 0.3
, HUnit >= 1.6 && < 1.7
, QuickCheck >= 2.10 && < 2.11
, stm >= 2.4 && < 2.5
, stm-chans >= 3.0 && < 3.1
, string-conversions >= 0.4 && < 0.5
, test-framework >= 0.8 && < 0.9
, test-framework-quickcheck2 >= 0.3 && < 0.4
, test-framework-hunit >= 0.3 && < 0.4
hs-source-dirs: test
ghc-options: -Wall
executable example-inproc-wallet-server
if flag(library-only)
Buildable: False
main-is: Main.hs
hs-source-dirs: examples/embedded-inproc-wallet-server/
extensions: OverloadedStrings
ghc-options: -Wall
-O3
-threaded
-rtsopts
-with-rtsopts=-N4
build-depends: base >= 4.8 && < 5
, aeson >= 1.2 && < 1.3
, aeson-pretty >= 0.7 && < 0.9
, haskoin-node >= 0.3 && < 0.5
, haskoin-wallet
, monad-logger >= 0.3 && < 0.4
, persistent-sqlite >= 2.2 && < 2.7
, resourcet >= 1.1 && < 1.2
, unordered-containers >= 0.2 && < 0.3
, string-conversions >= 0.4 && < 0.5
, zeromq4-haskell >= 0.7 && < 0.8

View File

@ -1,110 +0,0 @@
{-|
This package provides a command line application called /hw/ (haskoin
wallet). It is a lightweight bitcoin wallet featuring BIP32 key management,
deterministic signatures (RFC-6979) and first order support for
multisignature transactions. A library API for /hw/ is also exposed.
-}
module Network.Haskoin.Wallet
(
-- *Client
clientMain
, OutputFormat(..)
, Config(..)
-- *Server
, runSPVServer
, SPVMode(..)
-- *API JSON Types
, JsonAccount(..)
, JsonAddr(..)
, JsonCoin(..)
, JsonTx(..)
-- *API Request Types
, WalletRequest(..)
, ListRequest(..)
, NewAccount(..)
, OfflineTxData(..)
, CoinSignData(..)
, CreateTx(..)
, SignTx(..)
, NodeAction(..)
, AccountType(..)
, AddressType(..)
, addrTypeIndex
, TxType(..)
, TxConfidence(..)
, AddressInfo(..)
, BalanceInfo(..)
-- *API Response Types
, WalletResponse(..)
, TxCompleteRes(..)
, ListResult(..)
, RescanRes(..)
-- *Database Accounts
, initWallet
, accounts
, newAccount
, addAccountKeys
, getAccount
, isMultisigAccount
, isReadAccount
, isCompleteAccount
, defaultDeriv
, rootToAccKey
, rootToAccKeys
-- *Database Addresses
, getAddress
, addressesAll
, addresses
, addressList
, unusedAddresses
, addressCount
, setAddrLabel
, addressPrvKey
, useAddress
, setAccountGap
, firstAddrTime
, getPathRedeem
, getPathPubKey
-- *Database Bloom Filter
, getBloomFilter
-- *Database transactions
, txs
, addrTxs
, getTx
, getAccountTx
, importTx
, importNetTx
, signAccountTx
, createWalletTx
, signOfflineTx
, getOfflineTxData
, isCoinbaseTx
-- *Database blocks
, importMerkles
, walletBestBlock
-- *Database coins and balances
, spendableCoins
, accountBalance
, addressBalances
-- *Rescan
, resetRescan
) where
import Network.Haskoin.Wallet.Client
import Network.Haskoin.Wallet.Server
import Network.Haskoin.Wallet.Settings
import Network.Haskoin.Wallet.Types
import Network.Haskoin.Wallet.Accounts
import Network.Haskoin.Wallet.Transaction

View File

@ -1,741 +0,0 @@
module Network.Haskoin.Wallet.Accounts
(
-- *Database Wallet
initWallet
-- *Database Accounts
, accounts
, newAccount
, renameAccount
, addAccountKeys
, getAccount
, isMultisigAccount
, isReadAccount
, isCompleteAccount
-- *Database Addresses
, getAddress
, addressesAll
, addresses
, addressList
, unusedAddresses
, lookupByPubKey
, addressCount
, setAddrLabel
, addressPrvKey
, useAddress
, generateAddrs
, setAccountGap
, firstAddrTime
, getPathRedeem
, getPathPubKey
-- *Database Bloom Filter
, getBloomFilter
-- * Helpers
, defaultDeriv
, rootToAccKey
, rootToAccKeys
, subSelectAddrCount
) where
import Control.Applicative ((<|>))
import Control.Exception (throw)
import Control.Monad (unless, void, when)
import Control.Monad.Base (MonadBase)
import Control.Monad.Catch (MonadThrow, throwM)
import Control.Monad.Trans (MonadIO, liftIO)
import Control.Monad.Trans.Resource (MonadResource)
import Data.Default (def)
import Data.List (nub)
import Data.Maybe (fromMaybe, isJust, isNothing,
listToMaybe, mapMaybe,
maybeToList)
import Data.Serialize (encode)
import Data.String.Conversions (cs)
import Data.Text (Text, unpack)
import Data.Time.Clock (getCurrentTime)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import Data.Word (Word32, Word8)
import Database.Esqueleto (Entity (..), SqlExpr,
SqlPersistT, Value (..), asc,
case_, count, countDistinct,
countRows, desc, else_, from,
get, insertUnique, insert_,
limit, max_, offset, orderBy,
select, sub_select, then_,
unValue, val, when_, where_,
(&&.), (-.), (<.), (==.),
(>.), (^.))
import qualified Database.Persist as P (update, updateWhere,
(=.))
import Network.Haskoin.Block
import Network.Haskoin.Constants
import Network.Haskoin.Crypto
import Network.Haskoin.Network
import Network.Haskoin.Script
import Network.Haskoin.Wallet.Model
import Network.Haskoin.Wallet.Settings
import Network.Haskoin.Wallet.Types
{- Initialization -}
initWallet :: MonadIO m => Double -> SqlPersistT m ()
initWallet fpRate = do
prevConfigRes <- select $ from $ \c -> return $ count $ c ^. WalletStateId
let cnt = maybe 0 unValue $ listToMaybe prevConfigRes
when (cnt == (0 :: Int)) $ do
time <- liftIO getCurrentTime
-- Create an initial bloom filter
-- TODO: Compute a random nonce
let bloom = bloomCreate (filterLen 0) fpRate 0 BloomUpdateNone
insert_ WalletState
{ walletStateHeight = 0
, walletStateBlock = headerHash genesisHeader
, walletStateBloomFilter = bloom
, walletStateBloomElems = 0
, walletStateBloomFp = fpRate
, walletStateVersion = 1
, walletStateCreated = time
}
{- Account -}
defaultDeriv :: KeyIndex -> HardPath
defaultDeriv a = Deriv :| 44 :| bip44Coin :| a
rootToAccKey :: XPrvKey -> KeyIndex -> XPrvKey
rootToAccKey xKey deriv
-- use bip44 derivation for root keys
| xPrvDepth xKey == 0 = derivePath (defaultDeriv deriv) xKey
-- use custom keys as is
| otherwise = xKey
rootToAccKeys :: XPrvKey -> [XPubKey] -> [XPrvKey]
rootToAccKeys xKey pubs =
nub $ map (rootToAccKey xKey) is
where
is = nub $ map xPubChild pubs
-- | Fetch all accounts
accounts :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> ListRequest -> SqlPersistT m ([Account], Word32)
accounts ListRequest{..} = do
cntRes <- select $ from $ \acc ->
return $ countDistinct $ acc ^. AccountId
let cnt = maybe 0 unValue $ listToMaybe cntRes
when (listOffset > 0 && listOffset >= cnt) $ throw $ WalletException
"Offset beyond end of data set"
res <- fmap (map entityVal) $ select $ from $ \acc -> do
limitOffset listLimit listOffset
return acc
return (res, cnt)
initGap :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> Entity Account -> SqlPersistT m ()
initGap accE = do
void $ createAddrs accE AddressExternal 20
void $ createAddrs accE AddressInternal 20
genMnemonic :: (MonadThrow m, MonadIO m) => Word8 -> m Mnemonic
genMnemonic bytes
| bytes `elem` [16,20..32] = do
entropy <- liftIO $ getEntropy $ fromIntegral bytes
case toMnemonic entropy of
Right ms -> return ms
Left err -> throwM $ WalletException err
| otherwise = throwM $
WalletException "Entropy can only be 16, 20, 24, 28 or 32 bytes"
mnemonicToPrvKey :: MonadThrow m => Passphrase -> Mnemonic -> m XPrvKey
mnemonicToPrvKey pass ms = case mnemonicToSeed pass ms of
Right seed -> return $ makeXPrvKey seed
Left err -> throwM $ WalletException err
-- |Create a new account using the given mnemonic or keys. If no mnemonic,
-- master key or public keys are provided, a new mnemonic will be generated.
newAccount :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> NewAccount
-> SqlPersistT m (Entity Account, Maybe Mnemonic)
newAccount NewAccount{..} = do
unless (validAccountType newAccountType) $
throwM $ WalletException "Invalid account type"
when (isJust newAccountMnemonic && isJust newAccountMaster) $ throwM $
WalletException "Can not set both master key and mnemonic"
let pass = cs $ fromMaybe "" newAccountPassword
(mnemonicM, keyM) <- case newAccountMaster of
Just xPrv -> return (Nothing, Just xPrv)
_ -> case newAccountMnemonic of
Just ms -> do
root <- mnemonicToPrvKey pass $ cs ms
-- Don't return our own mnemonic as we don't want it displayed
return (Nothing, Just root)
_ -> if null newAccountKeys
then do
-- Generate a key if nothing was provided
let defEnt = configEntropy def
ms <- genMnemonic $ fromMaybe defEnt newAccountEntropy
root <- mnemonicToPrvKey pass ms
return (Just ms, Just root)
else return (Nothing, Nothing)
-- TODO: If account already exists, increase the derivation index
let d = fromMaybe 0 newAccountDeriv
accKeyM = (`rootToAccKey` d) <$> keyM
accPubKeys = maybeToList (deriveXPubKey <$> accKeyM) ++ newAccountKeys
-- Build the account
now <- liftIO getCurrentTime
let acc = Account
{ accountName = newAccountName
, accountType = newAccountType
-- Never store private keys for read only accounts
, accountMaster = if newAccountReadOnly
then Nothing
else accKeyM
, accountKeys = nub accPubKeys
, accountGap = 0
, accountCreated = now
}
-- Check if all the keys are valid
unless (isValidAccKeys acc) $
throwM $ WalletException "Invalid account keys"
-- Insert our account in the database
let canSetGap = isCompleteAccount acc
newAcc = acc{ accountGap = if canSetGap then 10 else 0 }
insertUnique newAcc >>= \resM -> case resM of
-- The account got created.
Just ai -> do
let accE = Entity ai newAcc
-- If we can set the gap, create the gap addresses
when canSetGap $ initGap accE
-- The mnemonic is returned to eventually be displayed as it is
-- not stored anywhere.
return (accE, mnemonicM)
-- The account already exists
Nothing -> throwM $ WalletException "Account already exists"
renameAccount :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> Entity Account
-> AccountName
-> SqlPersistT m Account
renameAccount (Entity ai acc) name = do
P.update ai [ AccountName P.=. name ]
return $ acc{ accountName = name }
-- | Add new thirdparty keys to a multisignature account. This function can
-- fail if the multisignature account already has all required keys.
addAccountKeys :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> Entity Account -- ^ Account Entity
-> [XPubKey] -- ^ Thirdparty public keys to add
-> SqlPersistT m Account -- ^ Account information
addAccountKeys (Entity ai acc) keys
-- We can only add keys on incomplete accounts
| isCompleteAccount acc = throwM $
WalletException "The account is already complete"
| null keys || not (isValidAccKeys accKeys) = throwM $
WalletException "Invalid account keys"
| otherwise = do
let canSetGap = isCompleteAccount accKeys
updGap = [ AccountGap P.=. 10 | canSetGap ]
newAcc = accKeys{ accountGap = if canSetGap then 10 else 0 }
-- Update the account with the keys and the new gap if it is complete
P.update ai $ (AccountKeys P.=. newKeys) : updGap
-- If we can set the gap, create the gap addresses
when canSetGap $ initGap $ Entity ai newAcc
return newAcc
where
newKeys = accountKeys acc ++ keys
accKeys = acc{ accountKeys = newKeys }
isValidAccKeys :: Account -> Bool
isValidAccKeys Account{..} = testMaster && case accountType of
AccountRegular -> length accountKeys == 1
AccountMultisig _ n -> goMultisig n
where
goMultisig n =
length accountKeys == length (nub accountKeys) &&
length accountKeys <= n && not (null accountKeys)
testMaster = case accountMaster of
Just m -> deriveXPubKey m `elem` accountKeys
Nothing -> True
-- Helper functions to get an Account if it exists, or throw an exception
-- otherwise.
getAccount :: (MonadIO m, MonadThrow m) => AccountName
-> SqlPersistT m (Entity Account)
getAccount accountName = do
as <- select $ from $ \a -> do
where_ $ a ^. AccountName ==. val accountName
return a
case as of
(accEnt:_) -> return accEnt
_ -> throwM $ WalletException $ unwords
[ "Account", unpack accountName, "does not exist" ]
{- Addresses -}
-- | Get an address if it exists, or throw an exception otherwise. Fetching
-- addresses in the hidden gap will also throw an exception.
getAddress :: (MonadIO m, MonadThrow m)
=> Entity Account -- ^ Account Entity
-> AddressType -- ^ Address type
-> KeyIndex -- ^ Derivation index (key)
-> SqlPersistT m (Entity WalletAddr) -- ^ Address
getAddress accE@(Entity ai _) addrType index = do
res <- select $ from $ \x -> do
where_ ( x ^. WalletAddrAccount ==. val ai
&&. x ^. WalletAddrType ==. val addrType
&&. x ^. WalletAddrIndex ==. val index
&&. x ^. WalletAddrIndex <. subSelectAddrCount accE addrType
)
limit 1
return x
case res of
(addrE:_) -> return addrE
_ -> throwM $ WalletException $ unwords
[ "Invalid address index", show index ]
-- | All addresses in the wallet, including hidden gap addresses. This is useful
-- for building a bloom filter.
addressesAll :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> SqlPersistT m [WalletAddr]
addressesAll = fmap (map entityVal) $ select $ from return
-- | All addresses in one account excluding hidden gap.
addresses :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> Entity Account -- ^ Account Entity
-> AddressType -- ^ Address Type
-> SqlPersistT m [WalletAddr] -- ^ Addresses
addresses accE@(Entity ai _) addrType = fmap (map entityVal) $
select $ from $ \x -> do
where_ ( x ^. WalletAddrAccount ==. val ai
&&. x ^. WalletAddrType ==. val addrType
&&. x ^. WalletAddrIndex <. subSelectAddrCount accE addrType
)
return x
-- | Get address list.
addressList :: MonadIO m
=> Entity Account -- ^ Account Entity
-> AddressType -- ^ Address type
-> ListRequest -- ^ List request
-> SqlPersistT m ([WalletAddr], Word32)
-- ^ List result
addressList accE@(Entity ai _) addrType ListRequest{..} = do
cnt <- addressCount accE addrType
when (listOffset > 0 && listOffset >= cnt) $ throw $ WalletException
"Offset beyond end of data set"
res <- fmap (map entityVal) $ select $ from $ \x -> do
where_ ( x ^. WalletAddrAccount ==. val ai
&&. x ^. WalletAddrType ==. val addrType
&&. x ^. WalletAddrIndex <. val cnt
)
let order = if listReverse then asc else desc
orderBy [ order (x ^. WalletAddrIndex) ]
when (listLimit > 0) $ limit $ fromIntegral listLimit
when (listOffset > 0) $ offset $ fromIntegral listOffset
return x
return (res, cnt)
-- | Get a count of all the addresses in an account
addressCount :: MonadIO m
=> Entity Account -- ^ Account Entity
-> AddressType -- ^ Address type
-> SqlPersistT m Word32 -- ^ Address Count
addressCount (Entity ai acc) addrType = do
res <- select $ from $ \x -> do
where_ ( x ^. WalletAddrAccount ==. val ai
&&. x ^. WalletAddrType ==. val addrType
)
return countRows
let cnt = maybe 0 unValue $ listToMaybe res
return $ if cnt > accountGap acc then cnt - accountGap acc else 0
-- | Get a list of all unused addresses.
unusedAddresses :: MonadIO m
=> Entity Account -- ^ Account ID
-> AddressType -- ^ Address type
-> ListRequest
-> SqlPersistT m ([WalletAddr], Word32) -- ^ Unused addresses
unusedAddresses (Entity ai acc) addrType ListRequest{..} = do
cntRes <- select $ from $ \x -> do
where_ ( x ^. WalletAddrAccount ==. val ai
&&. x ^. WalletAddrType ==. val addrType
)
return countRows
let cnt = maybe 0 unValue $ listToMaybe cntRes
when (listOffset > 0 && listOffset >= gap) $ throw $ WalletException
"Offset beyond end of data set"
res <- fmap (map entityVal) $ select $ from $ \x -> do
where_ ( x ^. WalletAddrAccount ==. val ai
&&. x ^. WalletAddrType ==. val addrType
)
orderBy [ order $ x ^. WalletAddrIndex ]
limit $ fromIntegral $ lim cnt
offset $ fromIntegral $ off cnt
return x
return (res, gap)
where
gap = accountGap acc
lim' = if listLimit > 0 then listLimit else gap
off cnt | listReverse = listOffset + gap
| otherwise = cnt - 2 * gap + listOffset
lim cnt | listReverse = min lim' (gap - listOffset)
| otherwise = min lim' (cnt - off cnt - gap)
order = if listReverse then desc else asc
lookupByPubKey :: (MonadIO m, MonadThrow m)
=> Entity Account -- ^ Account Entity
-> PubKeyC -- ^ Pubkey of interest
-> AddressType -- ^ Address type
-> SqlPersistT m [WalletAddr]
lookupByPubKey (Entity ai _) key addrType =
fmap (map entityVal) $ select $ from $ \x -> do
where_ ( x ^. WalletAddrAccount ==. val ai
&&. x ^. WalletAddrType ==. val addrType
&&. x ^. WalletAddrKey ==. val (Just key)
)
return x
-- | Add a label to an address.
setAddrLabel :: (MonadIO m, MonadThrow m)
=> Entity Account -- ^ Account ID
-> KeyIndex -- ^ Derivation index
-> AddressType -- ^ Address type
-> Text -- ^ New label
-> SqlPersistT m WalletAddr
setAddrLabel accE i addrType label = do
Entity addrI addr <- getAddress accE addrType i
P.update addrI [ WalletAddrLabel P.=. label ]
return $ addr{ walletAddrLabel = label }
-- | Returns the private key of an address.
addressPrvKey :: (MonadIO m, MonadThrow m)
=> Entity Account -- ^ Account Entity
-> Maybe XPrvKey -- ^ If not in account
-> KeyIndex -- ^ Derivation index of the address
-> AddressType -- ^ Address type
-> SqlPersistT m PrvKeyC -- ^ Private key
addressPrvKey accE@(Entity ai acc) masterM index addrType = do
ret <- select $ from $ \x -> do
where_ ( x ^. WalletAddrAccount ==. val ai
&&. x ^. WalletAddrType ==. val addrType
&&. x ^. WalletAddrIndex ==. val index
&&. x ^. WalletAddrIndex <. subSelectAddrCount accE addrType
)
return $ x ^. WalletAddrIndex
case ret of
(Value idx:_) -> do
accKey <- case accountMaster acc <|> masterM of
Just key -> return key
Nothing -> throwM $ WalletException "Could not get private key"
let addrKey =
prvSubKey (prvSubKey accKey (addrTypeIndex addrType)) idx
return $ xPrvKey addrKey
_ -> throwM $ WalletException "Invalid address"
-- | Create new addresses in an account and increment the internal bloom filter.
-- This is a low-level function that simply creates the desired amount of new
-- addresses in an account, disregarding visible and hidden address gaps. You
-- should use the function `setAccountGap` if you want to control the gap of an
-- account instead.
createAddrs :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> Entity Account
-> AddressType
-> Word32
-> SqlPersistT m [WalletAddr]
createAddrs (Entity ai acc) addrType n
| n == 0 = throwM $ WalletException $
unwords [ "Invalid value", show n ]
| not (isCompleteAccount acc) =
throwM $ WalletException $ unwords
[ "Keys are still missing from the incomplete account"
, unpack $ accountName acc
]
| otherwise = do
now <- liftIO getCurrentTime
-- Find the next derivation index from the last address
lastRes <- select $ from $ \x -> do
where_ ( x ^. WalletAddrAccount ==. val ai
&&. x ^. WalletAddrType ==. val addrType
)
return $ max_ (x ^. WalletAddrIndex)
let nextI = case lastRes of
(Value (Just lastI):_) -> lastI + 1
_ -> 0
build (addr, keyM, rdmM, i) = WalletAddr
{ walletAddrAccount = ai
, walletAddrAddress = addr
, walletAddrIndex = i
, walletAddrType = addrType
, walletAddrLabel = ""
, walletAddrRedeem = rdmM
, walletAddrKey = keyM
, walletAddrCreated = now
}
res = map build $ take (fromIntegral n) $ deriveFrom nextI
-- Save the addresses and increment the bloom filter
splitInsertMany_ res
incrementFilter res
return res
where
-- Branch type (external = 0, internal = 1)
branchType = addrTypeIndex addrType
deriveFrom = case accountType acc of
AccountMultisig m _ ->
let f (a, r, i) = (a, Nothing, Just r, i)
deriv = Deriv :/ branchType
in map f . derivePathMSAddrs (accountKeys acc) deriv m
AccountRegular -> case accountKeys acc of
(key:_) -> let f (a, k, i) = (a, Just k, Nothing, i)
in map f . derivePathAddrs key (Deriv :/ branchType)
[] -> throw $ WalletException $ unwords
[ "createAddrs: No key in regular account (corrupt database)"
, unpack $ accountName acc
]
-- | Generate all the addresses up to certain index.
generateAddrs :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> Entity Account
-> AddressType
-> KeyIndex
-> SqlPersistT m Int
generateAddrs accE addrType genIndex = do
cnt <- addressCount accE addrType
let toGen = fromIntegral genIndex - fromIntegral cnt + 1
if toGen > 0
then do
void $ createAddrs accE addrType $ fromIntegral toGen
return toGen
else return 0
-- | Use an address and make sure we have enough gap addresses after it.
-- Returns the new addresses that have been created.
useAddress :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> WalletAddr -> SqlPersistT m [WalletAddr]
useAddress WalletAddr{..} = do
res <- select $ from $ \x -> do
where_ ( x ^. WalletAddrAccount ==. val walletAddrAccount
&&. x ^. WalletAddrType ==. val walletAddrType
&&. x ^. WalletAddrIndex >. val walletAddrIndex
)
return countRows
case res of
(Value cnt:_) -> get walletAddrAccount >>= \accM -> case accM of
Just acc -> do
let accE = Entity walletAddrAccount acc
gap = fromIntegral (accountGap acc) :: Int
missing = 2*gap - cnt
if missing > 0
then createAddrs accE walletAddrType $ fromIntegral missing
else return []
_ -> return [] -- Should not happen
_ -> return [] -- Should not happen
-- | Set the address gap of an account to a new value. This will create new
-- internal and external addresses as required. The gap can only be increased,
-- not decreased in size.
setAccountGap :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> Entity Account -- ^ Account Entity
-> Word32 -- ^ New gap value
-> SqlPersistT m (Entity Account)
setAccountGap accE@(Entity ai acc) gap
| not (isCompleteAccount acc) =
throwM $ WalletException $ unwords
[ "Keys are still missing from the incomplete account"
, unpack $ accountName acc
]
| missing <= 0 = throwM $ WalletException
"The gap of an account can only be increased"
| otherwise = do
_ <- createAddrs accE AddressExternal $ fromInteger $ missing*2
_ <- createAddrs accE AddressInternal $ fromInteger $ missing*2
P.update ai [ AccountGap P.=. gap ]
return $ Entity ai acc{ accountGap = gap }
where
missing = toInteger gap - toInteger (accountGap acc)
-- Return the creation time of the first address in the wallet.
firstAddrTime :: MonadIO m => SqlPersistT m (Maybe Timestamp)
firstAddrTime = do
res <- select $ from $ \x -> do
orderBy [ asc (x ^. WalletAddrId) ]
limit 1
return $ x ^. WalletAddrCreated
return $ case res of
(Value d:_) -> Just $ toPOSIX d
_ -> Nothing
where
toPOSIX = fromInteger . round . utcTimeToPOSIXSeconds
{- Bloom filters -}
-- | Add the given addresses to the bloom filter. If the number of elements
-- becomes too large, a new bloom filter is computed from scratch.
incrementFilter :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> [WalletAddr]
-> SqlPersistT m ()
incrementFilter addrs = do
(bloom, elems, _) <- getBloomFilter
let newElems = elems + (length addrs * 2)
if filterLen newElems > filterLen elems
then computeNewFilter
else setBloomFilter (addToFilter bloom addrs) newElems
-- | Generate a new bloom filter from the data in the database
computeNewFilter :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)
=> SqlPersistT m ()
computeNewFilter = do
(_, _, fpRate) <- getBloomFilter
-- Create a new empty bloom filter
-- TODO: Choose a random nonce for the bloom filter
-- TODO: Check global bloom filter length limits
cntRes <- select $ from $ \x -> return $ count $ x ^. WalletAddrId
let elems = maybe 0 unValue $ listToMaybe cntRes
newBloom = bloomCreate (filterLen elems) fpRate 0 BloomUpdateNone
addrs <- addressesAll
let bloom = addToFilter newBloom addrs
setBloomFilter bloom elems
-- Compute the size of a filter given a number of elements. Scale
-- the filter length by powers of 2.
filterLen :: Int -> Int
filterLen = round . pow2 . ceiling . log2
where
pow2 x = (2 :: Double) ** fromInteger x
log2 x = logBase (2 :: Double) (fromIntegral x)
-- | Add elements to a bloom filter
addToFilter :: BloomFilter -> [WalletAddr] -> BloomFilter
addToFilter bloom addrs =
bloom3
where
pks = mapMaybe walletAddrKey addrs
rdms = mapMaybe walletAddrRedeem addrs
-- Add the Hash160 of the addresses
f1 b a = bloomInsert b $ encode $ getAddrHash a
bloom1 = foldl f1 bloom $ map walletAddrAddress addrs
-- Add the redeem scripts
f2 b r = bloomInsert b $ encodeOutputBS r
bloom2 = foldl f2 bloom1 rdms
-- Add the public keys
f3 b p = bloomInsert b $ encode p
bloom3 = foldl f3 bloom2 pks
-- | Returns a bloom filter containing all the addresses in this wallet. This
-- includes internal and external addresses. The bloom filter can be set on a
-- peer connection to filter the transactions received by that peer.
getBloomFilter :: (MonadIO m, MonadThrow m)
=> SqlPersistT m (BloomFilter, Int, Double)
getBloomFilter = do
res <- select $ from $ \c -> do
limit 1
return ( c ^. WalletStateBloomFilter
, c ^. WalletStateBloomElems
, c ^. WalletStateBloomFp
)
case res of
((Value b, Value n, Value fp):_) -> return (b, n, fp)
_ -> throwM $
WalletException "getBloomFilter: Database not initialized"
-- | Save a bloom filter and the number of elements it contains
setBloomFilter :: MonadIO m => BloomFilter -> Int -> SqlPersistT m ()
setBloomFilter bloom elems =
P.updateWhere [] [ WalletStateBloomFilter P.=. bloom
, WalletStateBloomElems P.=. elems
]
-- Helper function to compute the redeem script of a given derivation path
-- for a given multisig account.
getPathRedeem :: Account -> SoftPath -> RedeemScript
getPathRedeem acc@Account{..} deriv = case accountType of
AccountMultisig m _ -> if isCompleteAccount acc
then sortMulSig $ PayMulSig pubKeys m
else throw $ WalletException $ unwords
[ "getPathRedeem: Incomplete multisig account"
, unpack accountName
]
_ -> throw $ WalletException $ unwords
[ "getPathRedeem: Account", unpack accountName
, "is not a multisig account"
]
where
f = toPubKeyG . xPubKey . derivePubPath deriv
pubKeys = map f accountKeys
-- Helper function to compute the public key of a given derivation path for
-- a given non-multisig account.
getPathPubKey :: Account -> SoftPath -> PubKeyC
getPathPubKey acc@Account{..} deriv
| isMultisigAccount acc = throw $ WalletException $
unwords [ "getPathPubKey: Account", unpack accountName
, "is not a regular non-multisig account"
]
| otherwise = case accountKeys of
(key:_) -> xPubKey $ derivePubPath deriv key
_ -> throw $ WalletException $ unwords
[ "getPathPubKey: No keys are available in account"
, unpack accountName
]
{- Helpers -}
subSelectAddrCount :: Entity Account
-> AddressType
-> SqlExpr (Value KeyIndex)
subSelectAddrCount (Entity ai acc) addrType =
sub_select $ from $ \x -> do
where_ ( x ^. WalletAddrAccount ==. val ai
&&. x ^. WalletAddrType ==. val addrType
)
let gap = val $ accountGap acc
return $ case_
[ when_ (countRows >. gap)
then_ (countRows -. gap)
] (else_ $ val 0)
validMultisigParams :: Int -> Int -> Bool
validMultisigParams m n = n >= 1 && n <= 15 && m >= 1 && m <= n
validAccountType :: AccountType -> Bool
validAccountType t = case t of
AccountRegular -> True
AccountMultisig m n -> validMultisigParams m n
isMultisigAccount :: Account -> Bool
isMultisigAccount acc = case accountType acc of
AccountRegular -> False
AccountMultisig{} -> True
isReadAccount :: Account -> Bool
isReadAccount = isNothing . accountMaster
isCompleteAccount :: Account -> Bool
isCompleteAccount acc = case accountType acc of
AccountRegular -> length (accountKeys acc) == 1
AccountMultisig _ n -> length (accountKeys acc) == n

View File

@ -1,59 +0,0 @@
module Network.Haskoin.Wallet.Block where
import Control.Exception (throw)
import Control.Monad.Catch (MonadThrow, throwM)
import Control.Monad.Trans (MonadIO)
import Data.Maybe (fromMaybe)
import Database.Persist.Sql (SqlPersistT)
import Network.Haskoin.Block
import Network.Haskoin.Node.HeaderTree
import Network.Haskoin.Wallet.Model
import Network.Haskoin.Wallet.Transaction
import Network.Haskoin.Wallet.Types
mainChain :: (MonadIO m, MonadThrow m)
=> Either BlockHeight BlockHash
-> ListRequest
-> SqlPersistT m (ListResult NodeBlock)
mainChain blockE ListRequest{..} = do
bestHash <- fst <$> walletBestBlock
bestM <- getBlockByHash bestHash
best <- maybe (throwM $ WalletException "Could not find wallet best block")
return bestM
remoteNode <- case blockE of
Right h -> do
remoteNodeM <- getBlockByHash h
maybe (throwM $ WalletException "Colud not get remote node")
return remoteNodeM
Left h -> do
heightNodeM <- getBlockByHeight best h
maybe (throwM $ WalletException "Could not find bock height")
return heightNodeM
frst <- (+1) . nodeBlockHeight <$> splitBlock best remoteNode
if nodeBlockHeight best < frst
then return $ ListResult [] 0
else do
let cnt = nodeBlockHeight best - frst
limit = min listLimit (cnt - listOffset)
offset =
if listReverse
then cnt - listOffset - limit
else listOffset
nodes <- getBlocksFromHeight best limit (frst + offset)
return $ ListResult nodes cnt
blockTxs :: [NodeBlock] -> [WalletTx] -> [(NodeBlock, [WalletTx])]
blockTxs blocks transactions = reverse $ go [] blocks transactions
where
go bs [] _ = bs
go bs (n:ns) [] = go ((n,[]):bs) ns []
go [] (n:ns) xs = go [(n,[])] ns xs
go (b:bs) (n:ns) (x:xs)
| nodeHash (fst b) == blockHashOf x =
go ((fst b, x : snd b) : bs) (n:ns) xs
| nodeHash n == blockHashOf x =
go ((n, [x]) : b : bs) ns xs
| otherwise = go ((n, []) : b : bs) ns (x:xs)
blockHashOf t = fromMaybe
(throw $ WalletException "Unexpected unconfirmed transaction")
(walletTxConfirmedBy t)

View File

@ -1,265 +0,0 @@
module Network.Haskoin.Wallet.Client (clientMain) where
import System.FilePath ((</>))
import System.Directory (createDirectoryIfMissing)
import System.Posix.Directory (changeWorkingDirectory)
import System.Posix.Files
( setFileMode
, setFileCreationMask
, unionFileModes
, ownerModes
, groupModes
, otherModes
, fileExist
)
import System.Environment (getArgs, lookupEnv)
import System.Info (os)
import System.Console.GetOpt
( getOpt
, usageInfo
, OptDescr (Option)
, ArgDescr (NoArg, ReqArg)
, ArgOrder (Permute)
)
import Control.Monad (forM_)
import Control.Monad.Trans (liftIO)
import qualified Control.Monad.Reader as R (runReaderT)
import Data.Default (def)
import Data.FileEmbed (embedFile)
import Data.Yaml (decodeFileEither)
import Data.String.Conversions (cs)
import Network.Haskoin.Constants
import Network.Haskoin.Wallet.Settings
import Network.Haskoin.Wallet.Client.Commands
import Network.Haskoin.Wallet.Types
import System.Exit (exitFailure)
import System.IO (hPutStrLn, stderr)
import System.FilePath.Posix (isAbsolute)
usageHeader :: String
usageHeader = "Usage: hw [<options>] <command> [<args>]"
cmdHelp :: [String]
cmdHelp = lines $ cs $ $(embedFile "config/help")
warningMsg :: String
warningMsg = unwords
[ "!!!", "This software is experimental."
, "Use only small amounts of Bitcoins.", "!!!"
]
usage :: [String]
usage = warningMsg : usageInfo usageHeader options : cmdHelp
read' :: Read x => String -> String -> x
read' e s = case reads s of
[(x, "")] -> x
_ -> error e
options :: [OptDescr (Config -> Config)]
options =
[ Option "c" ["count"]
( ReqArg
(\s cfg -> cfg { configCount = read' "Could not parse count" s })
"INT"
) $ "Items per page. Default: " ++ show (configCount def)
, Option "m" ["minconf"]
( ReqArg
(\s cfg ->
cfg { configMinConf = read' "Colud not parse minconf" s }
) "INT"
) $ "Minimum confirmations. Default: "
++ show (configMinConf def)
, Option "f" ["fee"]
( ReqArg
(\s cfg -> cfg { configFee = read' "Could not parse fee" s })
"INT"
) $ "Fee per kilobyte. Default: " ++ show (configFee def)
, Option "R" ["rcptfee"]
(NoArg $ \cfg -> cfg { configRcptFee = True }) $
"Recipient pays fee. Default: " ++ show (configRcptFee def)
, Option "S" ["nosig"]
(NoArg $ \cfg -> cfg { configSignTx = False }) $
"Do not sign. Default: " ++ show (not $ configSignTx def)
, Option "i" ["internal"]
(NoArg $ \cfg -> cfg { configAddrType = AddressInternal }) $
"Internal addresses. Default: "
++ show (configAddrType def == AddressInternal)
, Option "k" ["pubkeys"]
(NoArg $ \cfg -> cfg { configDisplayPubKeys = True }) $
"Display public keys instead of addresses. Default: "
++ show (configDisplayPubKeys def)
, Option "o" ["offline"]
(NoArg $ \cfg -> cfg { configOffline = True }) $
"Offline balance. Default: " ++ show (configOffline def)
, Option "e" ["entropy"]
( ReqArg ( \s cfg -> cfg { configEntropy =
read' "Could not parse entropy" s
}
) "INT"
) $ "Entropy in Bytes between 16 and 32. Default: "
++ show (configEntropy def)
, Option "r" ["revpage"]
(NoArg $ \cfg -> cfg { configReversePaging = True }) $
"Reverse paging. Default: "
++ show (configReversePaging def)
, Option "I" ["index"]
( ReqArg ( \s cfg -> cfg { configDerivIndex =
read' "Could not parse index" s
}
) "INT"
) $ "Derivation index for new accounts. Default: "
++ show (configDerivIndex def)
, Option "j" ["json"]
(NoArg $ \cfg -> cfg { configFormat = OutputJSON })
"Output JSON"
, Option "y" ["yaml"]
(NoArg $ \cfg -> cfg { configFormat = OutputYAML })
"Output YAML"
, Option "s" ["socket"]
(ReqArg (\s cfg -> cfg { configConnect = s }) "URI") $
"Server socket. Default: " ++ configConnect def
, Option "d" ["detach"]
(NoArg $ \cfg -> cfg { configDetach = True }) $
"Detach server. Default: " ++ show (configDetach def)
, Option "t" ["testnet"]
(NoArg $ \cfg -> cfg { configTestnet = True }) "Testnet3 network"
, Option "g" ["config"]
(ReqArg (\s cfg -> cfg { configFile = s }) "FILE") $
"Config file. Default: " ++ configFile def
, Option "w" ["workdir"]
(ReqArg (\s cfg -> cfg { configDir = s }) "DIR")
"Working directory. OS-dependent default"
, Option "v" ["verbose"]
(NoArg $ \cfg -> cfg { configVerbose = True }) "Verbose output"
]
-- Create and change current working directory
setWorkDir :: Config -> IO ()
setWorkDir cfg = do
let workDir = configDir cfg </> networkName
_ <- setFileCreationMask $ otherModes `unionFileModes` groupModes
createDirectoryIfMissing True workDir
setFileMode workDir ownerModes
changeWorkingDirectory workDir
-- Build application configuration
getConfig :: [Config -> Config] -> IO Config
getConfig fs = do
-- Create initial configuration from defaults and command-line arguments
let initCfg = foldr ($) def fs
-- If working directory set in initial configuration, use it
dir <- case configDir initCfg of "" -> appDir
d -> return d
-- Make configuration file relative to working directory
let cfgFile = if isAbsolute (configFile initCfg)
then configFile initCfg
else dir </> configFile initCfg
-- Get configuration from file, if it exists
e <- fileExist cfgFile
if e then do
cfgE <- decodeFileEither cfgFile
case cfgE of
Left x -> error $ show x
-- Override settings from file using command-line
Right cfg -> return $ fixConfigDir (foldr ($) cfg fs) dir
else return $ fixConfigDir initCfg dir
where
-- If working directory not set, use default
fixConfigDir cfg dir = case configDir cfg of "" -> cfg{ configDir = dir }
_ -> cfg
clientMain :: IO ()
clientMain = getArgs >>= \args -> case getOpt Permute options args of
(fs, commands, []) -> do
cfg <- getConfig fs
if configTestnet cfg
then setTestnet
else setProdnet
setWorkDir cfg
dispatchCommand cfg commands
(_, _, msgs) -> forM_ (msgs ++ usage) putStrLn
dispatchCommand :: Config -> [String] -> IO ()
dispatchCommand cfg args = flip R.runReaderT cfg $ case args of
"start" : [] -> cmdStart
"stop" : [] -> cmdStop
"newacc" : name : [] -> cmdNewAcc False name []
"newread" : name : [] -> cmdNewAcc True name []
"newms" : name : m : n : [] -> cmdNewAcc False name [m, n]
"newreadms" : name : m : n : [] -> cmdNewAcc True name [m, n]
"addkey" : name : [] -> cmdAddKey name
"setgap" : name : gap : [] -> cmdSetGap name gap
"account" : name : [] -> cmdAccount name
"accounts" : page -> cmdAccounts page
"rename" : name : new : [] -> cmdRenameAcc name new
"list" : name : page -> cmdList name page
"unused" : name : page -> cmdUnused name page
"label" : name : index : label : [] -> cmdLabel name index label
"uri" : name : index : ls -> cmdURI name index ls
"txs" : name : page -> cmdTxs name page
"addrtxs" : name : index : page -> cmdAddrTxs name index page
"getindex" : name : key : [] -> cmdGetIndex name key
"genaddrs" : name : i : [] -> cmdGenAddrs name i
"send" : name : add : amnt : [] -> cmdSend name add amnt
"sendmany" : name : xs -> cmdSendMany name xs
"import" : name : [] -> cmdImport name
"sign" : name : txid : [] -> cmdSign name txid
"gettx" : name : txid : [] -> cmdGetTx name txid
"balance" : name : [] -> cmdBalance name
"getoffline" : name : txid : [] -> cmdGetOffline name txid
"signoffline" : name : [] -> cmdSignOffline name
"rescan" : rescantime -> cmdRescan rescantime
"deletetx" : txid : [] -> cmdDeleteTx txid
"sync" : name : block : page -> cmdSync name block page
"pending" : name : page -> cmdPending name page
"dead" : name : page -> cmdDead name page
"monitor" : name -> cmdMonitor name
"decodetx" : [] -> cmdDecodeTx
"dice" : rolls : [] -> cmdDice rolls
"status" : [] -> cmdStatus
"keypair" : [] -> cmdKeyPair
"blockinfo" : hashes -> cmdBlockInfo hashes
"version" : [] -> cmdVersion
"help" : [] -> liftIO $ forM_ usage (hPutStrLn stderr)
[] -> liftIO $ forM_ usage (hPutStrLn stderr)
_ -> liftIO $
forM_ ("Invalid command" : usage) (hPutStrLn stderr) >> exitFailure
appDir :: IO FilePath
appDir = case os of "mingw" -> windows
"mingw32" -> windows
"mingw64" -> windows
"darwin" -> osx
"linux" -> unix
_ -> unix
where
windows = do
localAppData <- lookupEnv "LOCALAPPDATA"
dirM <- case localAppData of
Nothing -> lookupEnv "APPDATA"
Just l -> return $ Just l
case dirM of
Just d -> return $ d </> "Haskoin Wallet"
Nothing -> return "."
osx = do
homeM <- lookupEnv "HOME"
case homeM of
Just home -> return $ home </> "Library"
</> "Application Support"
</> "Haskoin Wallet"
Nothing -> return "."
unix = do
homeM <- lookupEnv "HOME"
case homeM of
Just home -> return $ home </> ".hw"
Nothing -> return "."

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
{-# LANGUAGE CPP #-}
module Network.Haskoin.Wallet.Client.PrettyJson
( encodePretty )
where
import qualified Data.ByteString.Lazy as BL
import Data.Aeson (ToJSON)
import Data.Aeson.Encode.Pretty as Export (Config (..),
defConfig,
encodePretty')
-- aeson-pretty 0.8.0 introduces a new way to specify indentation
#if MIN_VERSION_aeson_pretty(0,8,0)
import Data.Aeson.Encode.Pretty as Export (Indent(..))
jsonIndent :: Indent
jsonIndent = Spaces 2
#else
jsonIndent :: Int
jsonIndent = 2
#endif
encodePretty :: ToJSON a => a -> BL.ByteString
encodePretty = encodePretty' defConfig{ confIndent = jsonIndent }

View File

@ -1,17 +0,0 @@
module Network.Haskoin.Wallet.Database where
import Control.Monad.Logger (MonadLoggerIO)
import Control.Monad.Trans.Control (MonadBaseControl)
import Database.Persist.Sql (ConnectionPool)
import Database.Persist.Sqlite (SqliteConf(..), createSqlitePool)
type DatabaseConfType = SqliteConf
getDatabasePool :: (MonadLoggerIO m, MonadBaseControl IO m)
=> DatabaseConfType -> m ConnectionPool
getDatabasePool conf = createSqlitePool (sqlDatabase conf) (sqlPoolSize conf)
paramLimit :: Int
paramLimit = 20

View File

@ -1,31 +0,0 @@
{-|
This module expose haskoin-wallet internals. No guarantee is made on the
stability of the interface of these internal modules.
-}
module Network.Haskoin.Wallet.Internals
( module Network.Haskoin.Wallet
, module Network.Haskoin.Wallet.Accounts
, module Network.Haskoin.Wallet.Transaction
, module Network.Haskoin.Wallet.Client
, module Network.Haskoin.Wallet.Client.Commands
, module Network.Haskoin.Wallet.Server
, module Network.Haskoin.Wallet.Server.Handler
, module Network.Haskoin.Wallet.Settings
, module Network.Haskoin.Wallet.Database
, module Network.Haskoin.Wallet.Types
, module Network.Haskoin.Wallet.Model
) where
import Network.Haskoin.Wallet
import Network.Haskoin.Wallet.Accounts
import Network.Haskoin.Wallet.Transaction
import Network.Haskoin.Wallet.Client
import Network.Haskoin.Wallet.Client.Commands
import Network.Haskoin.Wallet.Server
import Network.Haskoin.Wallet.Server.Handler
import Network.Haskoin.Wallet.Settings
import Network.Haskoin.Wallet.Database
import Network.Haskoin.Wallet.Types
import Network.Haskoin.Wallet.Model

View File

@ -1,132 +0,0 @@
module Network.Haskoin.Wallet.Model
( -- Database types
Account(..)
, AccountId
, WalletAddr(..)
, WalletAddrId
, WalletState(..)
, WalletStateId
, WalletCoin(..)
, WalletCoinId
, SpentCoin(..)
, SpentCoinId
, WalletTx(..)
, WalletTxId
, EntityField(..)
, Unique(..)
, migrateWallet
-- JSON conversion
, toJsonAccount
, toJsonAddr
, toJsonCoin
, toJsonTx
) where
import Data.Word (Word32, Word64)
import Data.Time (UTCTime)
import Data.Text (Text)
import Data.String.Conversions (cs)
import Database.Persist (EntityField, Unique)
import Database.Persist.Quasi (lowerCaseSettings)
import Database.Persist.TH
( share
, mkPersist
, sqlSettings
, mkMigrate
, persistFileWith
)
import Network.Haskoin.Wallet.Types
import Network.Haskoin.Block
import Network.Haskoin.Transaction
import Network.Haskoin.Script
import Network.Haskoin.Crypto
import Network.Haskoin.Network
share [ mkPersist sqlSettings
, mkMigrate "migrateWallet"
]
$(persistFileWith lowerCaseSettings "config/models")
{- JSON Types -}
toJsonAccount :: Maybe Mnemonic -> Account -> JsonAccount
toJsonAccount msM acc = JsonAccount
{ jsonAccountName = accountName acc
, jsonAccountType = accountType acc
, jsonAccountMnemonic = fmap cs msM
, jsonAccountMaster = accountMaster acc
, jsonAccountKeys = accountKeys acc
, jsonAccountGap = accountGap acc
, jsonAccountCreated = accountCreated acc
}
toJsonAddr :: WalletAddr
-> Maybe BalanceInfo
-> JsonAddr
toJsonAddr addr balM = JsonAddr
{ jsonAddrAddress = walletAddrAddress addr
, jsonAddrIndex = walletAddrIndex addr
, jsonAddrType = walletAddrType addr
, jsonAddrLabel = walletAddrLabel addr
, jsonAddrRedeem = walletAddrRedeem addr
, jsonAddrKey = walletAddrKey addr
, jsonAddrCreated = walletAddrCreated addr
, jsonAddrBalance = balM
}
toJsonTx :: AccountName
-> Maybe (BlockHash, BlockHeight) -- ^ Current best block
-> WalletTx
-> JsonTx
toJsonTx acc bbM tx = JsonTx
{ jsonTxHash = walletTxHash tx
, jsonTxNosigHash = walletTxNosigHash tx
, jsonTxType = walletTxType tx
, jsonTxInValue = walletTxInValue tx
, jsonTxOutValue = walletTxOutValue tx
, jsonTxValue = fromIntegral (walletTxInValue tx) -
fromIntegral (walletTxOutValue tx)
, jsonTxInputs = walletTxInputs tx
, jsonTxOutputs = walletTxOutputs tx
, jsonTxChange = walletTxChange tx
, jsonTxTx = walletTxTx tx
, jsonTxIsCoinbase = walletTxIsCoinbase tx
, jsonTxConfidence = walletTxConfidence tx
, jsonTxConfirmedBy = walletTxConfirmedBy tx
, jsonTxConfirmedHeight = walletTxConfirmedHeight tx
, jsonTxConfirmedDate = walletTxConfirmedDate tx
, jsonTxCreated = walletTxCreated tx
, jsonTxAccount = acc
, jsonTxConfirmations = f =<< walletTxConfirmedHeight tx
, jsonTxBestBlock = fst <$> bbM
, jsonTxBestBlockHeight = snd <$> bbM
}
where
f confirmedHeight = case bbM of
Just (_, h) -> return $ fromInteger $
max 0 $ toInteger h - toInteger confirmedHeight + 1
_ -> Nothing
toJsonCoin :: WalletCoin
-> Maybe JsonTx -- ^ Coins transaction
-> Maybe JsonAddr -- ^ Coins address
-> Maybe JsonTx -- ^ Coins spending transaction
-> JsonCoin
toJsonCoin coin txM addrM spendM = JsonCoin
{ jsonCoinHash = walletCoinHash coin
, jsonCoinPos = walletCoinPos coin
, jsonCoinValue = walletCoinValue coin
, jsonCoinScript = walletCoinScript coin
, jsonCoinCreated = walletCoinCreated coin
-- Optional tx
, jsonCoinTx = txM
-- Optional address
, jsonCoinAddress = addrM
-- Optional spending tx
, jsonCoinSpendingTx = spendM
}

View File

@ -1,502 +0,0 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
module Network.Haskoin.Wallet.Server
( runSPVServer
, runSPVServerWithContext
) where
import Control.Concurrent.Async.Lifted (async, link,
waitAnyCancel)
import Control.Concurrent.STM (atomically, retry)
import Control.Concurrent.STM.TBMChan (TBMChan, newTBMChan,
readTBMChan)
import Control.DeepSeq (NFData (..))
import Control.Exception.Lifted (ErrorCall (..),
SomeException (..),
catches)
import qualified Control.Exception.Lifted as E (Handler (..))
import Control.Monad (forM_, forever, unless,
void, when)
import Control.Monad.Base (MonadBase)
import Control.Monad.Catch (MonadThrow)
import Control.Monad.Fix (fix)
import Control.Monad.Logger (MonadLoggerIO,
filterLogger, logDebug,
logError, logInfo,
logWarn,
runStdoutLoggingT)
import Control.Monad.Trans (lift, liftIO)
import Control.Monad.Trans.Control (MonadBaseControl,
liftBaseOpDiscard)
import Control.Monad.Trans.Resource (MonadResource,
runResourceT)
import Data.Aeson (Value, decode, encode)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as BL (fromStrict,
toStrict)
import Data.Conduit (await, awaitForever,
($$))
import qualified Data.HashMap.Strict as H (lookup)
import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.Map.Strict as M (Map, assocs, elems,
empty,
fromListWith, null,
unionWith)
import Data.Maybe (fromJust, fromMaybe,
isJust)
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
import Data.Text (pack)
import Data.Word (Word32)
import Database.Esqueleto (from, val, where_,
(&&.), (<=.), (==.),
(^.))
import Database.Persist.Sql (ConnectionPool,
runMigration)
import Network.Haskoin.Block
import Network.Haskoin.Constants
import Network.Haskoin.Node.BlockChain
import Network.Haskoin.Node.HeaderTree
import Network.Haskoin.Node.Peer
import Network.Haskoin.Node.STM
import Network.Haskoin.Transaction
import Network.Haskoin.Wallet.Accounts
import Network.Haskoin.Wallet.Database
import Network.Haskoin.Wallet.Model
import Network.Haskoin.Wallet.Server.Handler
import Network.Haskoin.Wallet.Settings
import Network.Haskoin.Wallet.Transaction
import Network.Haskoin.Wallet.Types
import System.Posix.Daemon (Redirection (ToFile),
runDetached)
import System.ZMQ4 (Context, KeyFormat (..),
Pub (..), Rep (..),
Socket, bind, receive,
receiveMulti, restrict,
send, sendMulti,
setCurveSecretKey,
setCurveServer,
setLinger, withContext,
withSocket, z85Decode)
data EventSession = EventSession
{ eventBatchSize :: !Int
, eventNewAddrs :: !(M.Map AccountId Word32)
}
deriving (Eq, Show, Read)
instance NFData EventSession where
rnf EventSession{..} =
rnf eventBatchSize `seq`
rnf (M.elems eventNewAddrs)
runSPVServer :: Config -> IO ()
runSPVServer cfg = maybeDetach cfg $ -- start the server process
withContext (run . runSPVServerWithContext cfg)
where
-- Setup logging monads
run = runResourceT . runLogging
runLogging = runStdoutLoggingT . filterLogger logFilter
logFilter _ level = level >= configLogLevel cfg
-- |Run the server, and use the specifed ZeroMQ context.
-- Useful if you want to communicate with the server using
-- the "inproc" ZeroMQ transport, where a shared context is
-- required.
runSPVServerWithContext :: ( MonadLoggerIO m
, MonadBaseControl IO m
, MonadBase IO m
, MonadThrow m
, MonadResource m
)
=> Config -> Context -> m ()
runSPVServerWithContext cfg ctx = do
-- Initialize the database
-- Check the operation mode of the server.
pool <- initDatabase cfg
-- Notification channel
notif <- liftIO $ atomically $ newTBMChan 1000
case configMode cfg of
-- In this mode, we do not launch an SPV node. We only accept
-- client requests through the ZMQ API.
SPVOffline -> do
let session = HandlerSession cfg pool Nothing notif
as <- mapM async
-- Run the ZMQ API-command server
[ runWalletCmd ctx session
-- Run the ZMQ notification thread
, runWalletNotif ctx session
]
mapM_ link as
(_,r) <- waitAnyCancel as
return r
-- In this mode, we launch the client ZMQ API and we sync the
-- wallet database with an SPV node.
SPVOnline -> do
-- Initialize the node state
node <- getNodeState (Right pool)
-- Spin up the node threads
let session = HandlerSession cfg pool (Just node) notif
as <- mapM async
-- Start the SPV node
[ runNodeT (spv pool) node
-- Merkle block synchronization
, runNodeT (runMerkleSync pool notif) node
-- Import solo transactions as they arrive from peers
, runNodeT (txSource $$ processTx pool notif) node
-- Respond to transaction GetData requests
, runNodeT (handleGetData $ (`runDBPool` pool) . getTx) node
-- Re-broadcast pending transactions
, runNodeT (broadcastPendingTxs pool) node
-- Run the ZMQ API-command server
, runWalletCmd ctx session
-- Run the ZMQ notification thread
, runWalletNotif ctx session
]
mapM_ link as
(_,r) <- waitAnyCancel as
$(logDebug) "Exiting main thread"
return r
where
spv pool = do
-- Get our bloom filter
(bloom, elems, _) <- runDBPool getBloomFilter pool
startSPVNode hosts bloom elems
-- Bitcoin nodes to connect to
nodes = fromMaybe
(error $ "BTC nodes for " ++ networkName ++ " not found")
(pack networkName `H.lookup` configBTCNodes cfg)
hosts = map (\x -> PeerHost (btcNodeHost x) (btcNodePort x)) nodes
-- Run the merkle syncing thread
runMerkleSync pool notif = do
$(logDebug) "Waiting for a valid bloom filter for merkle downloads..."
-- Only download merkles if we have a valid bloom filter
_ <- atomicallyNodeT waitBloomFilter
-- Provide a fast catchup time if we are at height 0
fcM <- fmap (fmap adjustFCTime) $ (`runDBPool` pool) $ do
(_, h) <- walletBestBlock
if h == 0 then firstAddrTime else return Nothing
maybe (return ()) (atomicallyNodeT . rescanTs) fcM
-- Start the merkle sync
merkleSync pool 500 notif
$(logDebug) "Exiting Merkle-sync thread"
-- Run a thread that will re-broadcast pending transactions
broadcastPendingTxs pool = forever $ do
(hash, _) <- runSqlNodeT $ walletBestBlock
-- Wait until we are synced
atomicallyNodeT $ do
synced <- areBlocksSynced hash
unless synced $ lift retry
-- Send an INV for those transactions to all peers
broadcastTxs =<< runDBPool (getPendingTxs 0) pool
-- Wait until we are not synced
atomicallyNodeT $ do
synced <- areBlocksSynced hash
when synced $ lift retry
$(logDebug) "Exiting tx-broadcast thread"
processTx pool notif = do
awaitForever $ \tx -> lift $ do
(_, newAddrs) <- runDBPool (importNetTx tx (Just notif)) pool
unless (null newAddrs) $ do
$(logInfo) $ pack $ unwords
[ "Generated", show $ length newAddrs
, "new addresses while importing the tx."
, "Updating the bloom filter"
]
(bloom, elems, _) <- runDBPool getBloomFilter pool
atomicallyNodeT $ sendBloomFilter bloom elems
$(logDebug) "Exiting tx-import thread"
initDatabase :: (MonadBaseControl IO m, MonadLoggerIO m)
=> Config -> m ConnectionPool
initDatabase cfg = do
-- Create a database pool
let dbCfg = fromMaybe
(error $ "DB config settings for " ++ networkName ++ " not found")
(pack networkName `H.lookup` configDatabase cfg)
pool <- getDatabasePool dbCfg
-- Initialize wallet database
flip runDBPool pool $ do
_ <- runMigration migrateWallet
_ <- runMigration migrateHeaderTree
initWallet $ configBloomFP cfg
return pool
merkleSync
:: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m, MonadResource m)
=> ConnectionPool
-> Word32
-> TBMChan Notif
-> NodeT m ()
merkleSync pool bSize notif = do
-- Get our best block
(hash, _) <- runDBPool walletBestBlock pool
$(logDebug) "Starting merkle batch download"
-- Wait for a new block or a rescan
(action, source) <- merkleDownload hash bSize
$(logDebug) "Received a merkle action and source. Processing the source..."
-- Read and process the data from the source
(lastMerkleM, mTxsAcc, aMap) <- source $$ go Nothing [] M.empty
$(logDebug) "Merkle source processed and closed"
-- Send a new bloom filter to our peers if new addresses were generated
unless (M.null aMap) $ do
$(logInfo) $ pack $ unwords
[ "Generated", show $ sum $ M.elems aMap
, "new addresses while importing the merkle block."
, "Sending our bloom filter."
]
(bloom, elems, _) <- runDBPool getBloomFilter pool
atomicallyNodeT $ sendBloomFilter bloom elems
-- Check if we should rescan the current merkle batch
$(logDebug) "Checking if we need to rescan the current batch..."
rescan <- shouldRescan aMap
when rescan $ $(logDebug) "We need to rescan the current batch"
-- Compute the new batch size
let newBSize | rescan = max 1 $ bSize `div` 2
| otherwise = min 500 $ bSize + max 1 (bSize `div` 20)
when (newBSize /= bSize) $ $(logDebug) $ pack $ unwords
[ "Changing block batch size from", show bSize, "to", show newBSize ]
-- Did we receive all the merkles that we asked for ?
let missing = (headerHash <$> lastMerkleM) /=
Just (nodeHash $ last $ actionNodes action)
when missing $ $(logWarn) $ pack $ unwords
[ "Merkle block stream closed prematurely"
, show lastMerkleM
]
-- TODO: We could still salvage a partially received batch
unless (rescan || missing) $ do
$(logDebug) "Importing merkles into the wallet..."
-- Confirm the transactions
runDBPool (importMerkles action mTxsAcc (Just notif)) pool
$(logDebug) "Done importing merkles into the wallet"
logBlockChainAction action
merkleSync pool newBSize notif
where
go lastMerkleM mTxsAcc aMap = await >>= \resM -> case resM of
Just (Right tx) -> do
$(logDebug) $ pack $ unwords
[ "Importing merkle tx", cs $ txHashToHex $ txHash tx ]
(_, newAddrs) <- lift $ runDBPool (importNetTx tx Nothing) pool
$(logDebug) $ pack $ unwords
[ "Generated", show $ length newAddrs
, "new addresses while importing tx"
, cs $ txHashToHex $ txHash tx
]
let newMap = M.unionWith (+) aMap $ groupByAcc newAddrs
go lastMerkleM mTxsAcc newMap
Just (Left (MerkleBlock mHead _ _ _, mTxs)) -> do
$(logDebug) $ pack $ unwords
[ "Buffering merkle block"
, cs $ blockHashToHex $ headerHash mHead
]
go (Just mHead) (mTxs:mTxsAcc) aMap
-- Done processing this batch. Reverse mTxsAcc as we have been
-- prepending new values to it.
_ -> return (lastMerkleM, reverse mTxsAcc, aMap)
groupByAcc addrs =
let xs = map (\a -> (walletAddrAccount a, 1)) addrs
in M.fromListWith (+) xs
shouldRescan aMap = do
-- Try to find an account whos gap is smaller than the number of new
-- addresses generated in that account.
res <- (`runDBPool` pool) $ splitSelect (M.assocs aMap) $ \ks ->
from $ \a -> do
let andCond (ai, cnt) =
a ^. AccountId ==. val ai &&.
a ^. AccountGap <=. val cnt
where_ $ join2 $ map andCond ks
return $ a ^. AccountId
return $ not $ null res
-- Some logging of the blocks
logBlockChainAction action = case action of
BestChain nodes -> $(logInfo) $ pack $ unwords
[ "Best chain height"
, show $ nodeBlockHeight $ last nodes
, "(", cs $ blockHashToHex $ nodeHash $ last nodes
, ")"
]
ChainReorg _ o n -> $(logInfo) $ pack $ unlines $
[ "Chain reorg."
, "Orphaned blocks:"
]
++ map ((" " ++) . cs . blockHashToHex . nodeHash) o
++ [ "New blocks:" ]
++ map ((" " ++) . cs . blockHashToHex . nodeHash) n
++ [ unwords [ "Best merkle chain height"
, show $ nodeBlockHeight $ last n
]
]
SideChain n -> $(logWarn) $ pack $ unlines $
"Side chain:" :
map ((" " ++) . cs . blockHashToHex . nodeHash) n
KnownChain n -> $(logWarn) $ pack $ unlines $
"Known chain:" :
map ((" " ++) . cs . blockHashToHex . nodeHash) n
maybeDetach :: Config -> IO () -> IO ()
maybeDetach cfg action =
if configDetach cfg then runDetached pidFile logFile action >> logStarted else action
where
logStarted = putStrLn "Process started"
pidFile = Just $ configPidFile cfg
logFile = ToFile $ configLogFile cfg
-- Run the main ZeroMQ loop
-- TODO: Support concurrent requests using DEALER socket when we can do
-- concurrent MySQL requests.
runWalletNotif :: ( MonadLoggerIO m
, MonadBaseControl IO m
, MonadBase IO m
, MonadThrow m
, MonadResource m
)
=> Context -> HandlerSession -> m ()
runWalletNotif ctx session =
liftBaseOpDiscard (withSocket ctx Pub) $ \sock -> do
liftIO $ setLinger (restrict (0 :: Int)) sock
setupCrypto ctx sock session
liftIO $ bind sock $ configBindNotif $ handlerConfig session
forever $ do
xM <- liftIO $ atomically $ readTBMChan $ handlerNotifChan session
forM_ xM $ \x ->
let (typ, pay) = case x of
NotifBlock _ ->
("[block]", cs $ encode x)
NotifTx JsonTx{..} ->
("{" <> cs jsonTxAccount <> "}", cs $ encode x)
in liftIO $ sendMulti sock $ typ :| [pay]
runWalletCmd :: ( MonadLoggerIO m
, MonadBaseControl IO m
, MonadBase IO m
, MonadThrow m
, MonadResource m
)
=> Context -> HandlerSession -> m ()
runWalletCmd ctx session = do
liftBaseOpDiscard (withSocket ctx Rep) $ \sock -> do
liftIO $ setLinger (restrict (0 :: Int)) sock
setupCrypto ctx sock session
liftIO $ bind sock $ configBind $ handlerConfig session
fix $ \loop -> do
bs <- liftIO $ receive sock
let msg = decode $ BL.fromStrict bs
res <- case msg of
Just r -> catchErrors $
runHandler (dispatchRequest r) session
Nothing -> return $ ResponseError "Could not decode request"
liftIO $ send sock [] $ BL.toStrict $ encode res
unless (msg == Just StopServerReq) loop
$(logInfo) "Exiting ZMQ command thread..."
where
catchErrors m = catches m
[ E.Handler $ \(WalletException err) -> do
$(logError) $ pack err
return $ ResponseError $ pack err
, E.Handler $ \(ErrorCall err) -> do
$(logError) $ pack err
return $ ResponseError $ pack err
, E.Handler $ \(SomeException exc) -> do
$(logError) $ pack $ show exc
return $ ResponseError $ pack $ show exc
]
setupCrypto :: (MonadLoggerIO m, MonadBaseControl IO m)
=> Context -> Socket a -> HandlerSession -> m ()
setupCrypto ctx' sock session = do
when (isJust serverKeyM) $ liftIO $ do
let k = fromJust $ configServerKey $ handlerConfig session
setCurveServer True sock
setCurveSecretKey TextFormat k sock
when (isJust clientKeyPubM) $ do
k <- z85Decode (fromJust clientKeyPubM)
void $ async $ runZapAuth ctx' k
where
cfg = handlerConfig session
serverKeyM = configServerKey cfg
clientKeyPubM = configClientKeyPub cfg
runZapAuth :: ( MonadLoggerIO m
, MonadBaseControl IO m
, MonadBase IO m
)
=> Context -> ByteString -> m ()
runZapAuth ctx k = do
$(logDebug) $ "Starting ØMQ authentication thread"
liftBaseOpDiscard (withSocket ctx Rep) $ \zap -> do
liftIO $ setLinger (restrict (0 :: Int)) zap
liftIO $ bind zap "inproc://zeromq.zap.01"
forever $ do
buffer <- liftIO $ receiveMulti zap
let actionE =
case buffer of
v:q:_:_:_:m:p:_ -> do
when (v /= "1.0") $
Left (q, "500", "Version number not valid")
when (m /= "CURVE") $
Left (q, "400", "Mechanism not supported")
when (p /= k) $
Left (q, "400", "Invalid client public key")
return q
_ -> Left ("", "500", "Malformed request")
case actionE of
Right q -> do
$(logInfo) "Authenticated client successfully"
liftIO $ sendMulti zap $
"1.0" :| [q, "200", "OK", "client", ""]
Left (q, c, m) -> do
$(logError) $ pack $ unwords
[ "Failed to authenticate client:" , cs c, cs m ]
liftIO $ sendMulti zap $
"1.0" :| [q, c, m, "", ""]
dispatchRequest :: ( MonadLoggerIO m
, MonadBaseControl IO m
, MonadBase IO m
, MonadThrow m
, MonadResource m
)
=> WalletRequest -> Handler m (WalletResponse Value)
dispatchRequest req = fmap ResponseValid $ case req of
AccountReq n -> accountReq n
AccountsReq p -> accountsReq p
NewAccountReq na -> newAccountReq na
RenameAccountReq n n' -> renameAccountReq n n'
AddPubKeysReq n ks -> addPubKeysReq n ks
SetAccountGapReq n g -> setAccountGapReq n g
AddrsReq n t m o p -> addrsReq n t m o p
UnusedAddrsReq n t p -> unusedAddrsReq n t p
AddressReq n i t m o -> addressReq n i t m o
PubKeyIndexReq n k t -> pubKeyIndexReq n k t
SetAddrLabelReq n i t l -> setAddrLabelReq n i t l
GenerateAddrsReq n i t -> generateAddrsReq n i t
TxsReq n p -> txsReq n p
PendingTxsReq a p -> pendingTxsReq a p
DeadTxsReq a p -> deadTxsReq a p
AddrTxsReq n i t p -> addrTxsReq n i t p
CreateTxReq n c -> createTxReq n c
ImportTxReq n t -> importTxReq n t
SignTxReq n s -> signTxReq n s
TxReq n h -> txReq n h
DeleteTxReq t -> deleteTxReq t
OfflineTxReq n h -> offlineTxReq n h
SignOfflineTxReq n k t c -> signOfflineTxReq n k t c
BalanceReq n mc o -> balanceReq n mc o
NodeActionReq na -> nodeActionReq na
SyncReq a n b -> syncReq a (Right n) b
SyncHeightReq a n b -> syncReq a (Left n) b
BlockInfoReq l -> blockInfoReq l
StopServerReq -> stopServerReq

View File

@ -1,647 +0,0 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
module Network.Haskoin.Wallet.Server.Handler where
import Control.Arrow (first)
import Control.Concurrent.STM.TBMChan (TBMChan)
import Control.Exception (SomeException (..),
tryJust)
import Control.Monad (liftM, forM, unless, when)
import Control.Monad.Base (MonadBase)
import Control.Monad.Catch (MonadThrow, throwM)
import Control.Monad.Logger (MonadLoggerIO, logError,
logInfo)
import Control.Monad.Reader (ReaderT, asks, runReaderT)
import Control.Monad.Trans (MonadIO, lift, liftIO)
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Monad.Trans.Resource (MonadResource)
import Data.Aeson (Value (..), toJSON)
import qualified Data.Map.Strict as M (elems, fromList,
intersectionWith)
import Data.String.Conversions (cs)
import Data.Text (Text, pack, unpack)
import Data.Word (Word32)
import Data.Maybe (catMaybes)
import Database.Esqueleto (Entity (..), SqlPersistT)
import Database.Persist.Sql (ConnectionPool,
SqlPersistM,
runSqlPersistMPool,
runSqlPool)
import Network.Haskoin.Block
import Network.Haskoin.Crypto
import Network.Haskoin.Node.BlockChain
import Network.Haskoin.Node.HeaderTree
import Network.Haskoin.Node.Peer
import Network.Haskoin.Node.STM
import Network.Haskoin.Transaction
import Network.Haskoin.Wallet.Accounts
import Network.Haskoin.Wallet.Block
import Network.Haskoin.Wallet.Model
import Network.Haskoin.Wallet.Settings
import Network.Haskoin.Wallet.Transaction
import Network.Haskoin.Wallet.Types
import Network.Haskoin.Wallet.Types.BlockInfo (fromNodeBlock)
type Handler m = ReaderT HandlerSession m
data HandlerSession = HandlerSession
{ handlerConfig :: !Config
, handlerPool :: !ConnectionPool
, handlerNodeState :: !(Maybe SharedNodeState)
, handlerNotifChan :: !(TBMChan Notif)
}
runHandler :: Monad m => Handler m a -> HandlerSession -> m a
runHandler = runReaderT
runDB :: MonadBaseControl IO m => SqlPersistT m a -> Handler m a
runDB action = asks handlerPool >>= lift . runDBPool action
runDBPool :: MonadBaseControl IO m => SqlPersistT m a -> ConnectionPool -> m a
runDBPool = runSqlPool
tryDBPool :: MonadLoggerIO m => ConnectionPool -> SqlPersistM a -> m (Maybe a)
tryDBPool pool action = do
resE <- liftIO $ tryJust f $ runSqlPersistMPool action pool
case resE of
Right res -> return $ Just res
Left err -> do
$(logError) $ pack $ unwords [ "A database error occured:", err]
return Nothing
where
f (SomeException e) = Just $ show e
runNode :: MonadIO m => NodeT m a -> Handler m a
runNode action = do
nodeStateM <- asks handlerNodeState
case nodeStateM of
Just nodeState -> lift $ runNodeT action nodeState
_ -> error "runNode: No node state available"
{- Server Handlers -}
accountReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName -> Handler m (Maybe Value)
accountReq name = do
$(logInfo) $ format $ unlines
[ "Account"
, " Account name: " ++ unpack name
]
Entity _ acc <- runDB $ getAccount name
return $ Just $ toJSON $ toJsonAccount Nothing acc
accountsReq :: ( MonadLoggerIO m
, MonadBaseControl IO m
, MonadBase IO m
, MonadThrow m
, MonadResource m
)
=> ListRequest
-> Handler m (Maybe Value)
accountsReq lq@ListRequest{..} = do
$(logInfo) $ format $ unlines
[ "Accounts"
, " Offset : " ++ show listOffset
, " Limit : " ++ show listLimit
, " Reversed : " ++ show listReverse
]
(accs, cnt) <- runDB $ accounts lq
return $ Just $ toJSON $ ListResult (map (toJsonAccount Nothing) accs) cnt
newAccountReq
:: (MonadResource m, MonadThrow m, MonadLoggerIO m, MonadBaseControl IO m)
=> NewAccount -> Handler m (Maybe Value)
newAccountReq newAcc@NewAccount{..} = do
$(logInfo) $ format $ unlines
[ "NewAccount"
, " Account name: " ++ unpack newAccountName
, " Account type: " ++ show newAccountType
]
(Entity _ newAcc', mnemonicM) <- runDB $ newAccount newAcc
-- Update the bloom filter if the account is complete
whenOnline $ when (isCompleteAccount newAcc') updateNodeFilter
return $ Just $ toJSON $ toJsonAccount mnemonicM newAcc'
renameAccountReq
:: (MonadResource m, MonadThrow m, MonadLoggerIO m, MonadBaseControl IO m)
=> AccountName -> AccountName -> Handler m (Maybe Value)
renameAccountReq oldName newName = do
$(logInfo) $ format $ unlines
[ "RenameAccount"
, " Account name: " ++ unpack oldName
, " New name : " ++ unpack newName
]
newAcc <- runDB $ do
accE <- getAccount oldName
renameAccount accE newName
return $ Just $ toJSON $ toJsonAccount Nothing newAcc
addPubKeysReq
:: (MonadResource m, MonadThrow m, MonadLoggerIO m, MonadBaseControl IO m)
=> AccountName -> [XPubKey] -> Handler m (Maybe Value)
addPubKeysReq name keys = do
$(logInfo) $ format $ unlines
[ "AddPubKeys"
, " Account name: " ++ unpack name
, " Key count : " ++ show (length keys)
]
newAcc <- runDB $ do
accE <- getAccount name
addAccountKeys accE keys
-- Update the bloom filter if the account is complete
whenOnline $ when (isCompleteAccount newAcc) updateNodeFilter
return $ Just $ toJSON $ toJsonAccount Nothing newAcc
setAccountGapReq :: ( MonadLoggerIO m
, MonadBaseControl IO m
, MonadBase IO m
, MonadThrow m
, MonadResource m
)
=> AccountName -> Word32
-> Handler m (Maybe Value)
setAccountGapReq name gap = do
$(logInfo) $ format $ unlines
[ "SetAccountGap"
, " Account name: " ++ unpack name
, " New gap size: " ++ show gap
]
-- Update the gap
Entity _ newAcc <- runDB $ do
accE <- getAccount name
setAccountGap accE gap
-- Update the bloom filter
whenOnline updateNodeFilter
return $ Just $ toJSON $ toJsonAccount Nothing newAcc
addrsReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName
-> AddressType
-> Word32
-> Bool
-> ListRequest
-> Handler m (Maybe Value)
addrsReq name addrType minConf offline listReq = do
$(logInfo) $ format $ unlines
[ "Addresses"
, " Account name: " ++ unpack name
, " Address type: " ++ show addrType
, " Start index : " ++ show (listOffset listReq)
, " Reversed : " ++ show (listReverse listReq)
, " MinConf : " ++ show minConf
, " Offline : " ++ show offline
]
(res, bals, cnt) <- runDB $ do
accE <- getAccount name
(res, cnt) <- addressList accE addrType listReq
case res of
[] -> return (res, [], cnt)
_ -> do
let is = map walletAddrIndex res
(iMin, iMax) = (minimum is, maximum is)
bals <- addressBalances accE iMin iMax addrType minConf offline
return (res, bals, cnt)
-- Join addresses and balances together
let g (addr, bal) = toJsonAddr addr (Just bal)
addrBals = map g $ M.elems $ joinAddrs res bals
return $ Just $ toJSON $ ListResult addrBals cnt
where
joinAddrs addrs bals =
let f addr = (walletAddrIndex addr, addr)
in M.intersectionWith (,) (M.fromList $ map f addrs) (M.fromList bals)
unusedAddrsReq
:: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName -> AddressType -> ListRequest -> Handler m (Maybe Value)
unusedAddrsReq name addrType lq@ListRequest{..} = do
$(logInfo) $ format $ unlines
[ "UnusedAddrs"
, " Account name: " ++ unpack name
, " Address type: " ++ show addrType
, " Offset : " ++ show listOffset
, " Limit : " ++ show listLimit
, " Reversed : " ++ show listReverse
]
(addrs, cnt) <- runDB $ do
accE <- getAccount name
unusedAddresses accE addrType lq
return $ Just $ toJSON $ ListResult (map (`toJsonAddr` Nothing) addrs) cnt
addressReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName -> KeyIndex -> AddressType
-> Word32 -> Bool
-> Handler m (Maybe Value)
addressReq name i addrType minConf offline = do
$(logInfo) $ format $ unlines
[ "Address"
, " Account name: " ++ unpack name
, " Index : " ++ show i
, " Address type: " ++ show addrType
]
(addr, balM) <- runDB $ do
accE <- getAccount name
addrE <- getAddress accE addrType i
bals <- addressBalances accE i i addrType minConf offline
return $ case bals of
((_,bal):_) -> (entityVal addrE, Just bal)
_ -> (entityVal addrE, Nothing)
return $ Just $ toJSON $ toJsonAddr addr balM
-- TODO: How can we generalize this? Perhaps as part of wallet searching?
pubKeyIndexReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName
-> PubKeyC
-> AddressType
-> Handler m (Maybe Value)
pubKeyIndexReq name key addrType = do
$(logInfo) $ format $ unlines
[ "PubKeyIndex"
, " Account name: " ++ unpack name
, " Key : " ++ show key
, " Address type: " ++ show addrType
]
addrLst <- runDB $ do
accE <- getAccount name
lookupByPubKey accE key addrType
-- TODO: We don't return the balance for now. Maybe add it? Or not?
return $ Just $ toJSON $ map (`toJsonAddr` Nothing) addrLst
setAddrLabelReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName
-> KeyIndex
-> AddressType
-> Text
-> Handler m (Maybe Value)
setAddrLabelReq name i addrType label = do
$(logInfo) $ format $ unlines
[ "SetAddrLabel"
, " Account name: " ++ unpack name
, " Index : " ++ show i
, " Label : " ++ unpack label
]
newAddr <- runDB $ do
accE <- getAccount name
setAddrLabel accE i addrType label
return $ Just $ toJSON $ toJsonAddr newAddr Nothing
generateAddrsReq :: ( MonadLoggerIO m
, MonadBaseControl IO m
, MonadThrow m
, MonadBase IO m
, MonadResource m
)
=> AccountName
-> KeyIndex
-> AddressType
-> Handler m (Maybe Value)
generateAddrsReq name i addrType = do
$(logInfo) $ format $ unlines
[ "GenerateAddrs"
, " Account name: " ++ unpack name
, " Index : " ++ show i
]
cnt <- runDB $ do
accE <- getAccount name
generateAddrs accE addrType i
-- Update the bloom filter
whenOnline updateNodeFilter
return $ Just $ toJSON cnt
-- This is a generic function (see specifics below)
getTxs :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName
-> ListRequest
-> String
-> (AccountId -> ListRequest -> SqlPersistT m ([WalletTx], Word32))
-> Handler m (Maybe Value)
getTxs name lq@ListRequest{..} cmd f = do
$(logInfo) $ format $ unlines
[ cmd
, " Account name: " ++ unpack name
, " Offset : " ++ show listOffset
, " Limit : " ++ show listLimit
, " Reversed : " ++ show listReverse
]
(res, cnt, bb) <- runDB $ do
Entity ai _ <- getAccount name
bb <- walletBestBlock
(res, cnt) <- f ai lq
return (res, cnt, bb)
return $ Just $ toJSON $ ListResult (map (g bb) res) cnt
where
g bb = toJsonTx name (Just bb)
txsReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName -> ListRequest -> Handler m (Maybe Value)
txsReq name lq = getTxs name lq "Txs" (txs Nothing)
pendingTxsReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName -> ListRequest -> Handler m (Maybe Value)
pendingTxsReq name lq = getTxs name lq "PendingTxs" (txs (Just TxPending))
deadTxsReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName -> ListRequest -> Handler m (Maybe Value)
deadTxsReq name lq = getTxs name lq "DeadTxs" (txs (Just TxDead))
addrTxsReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName -> KeyIndex -> AddressType -> ListRequest
-> Handler m (Maybe Value)
addrTxsReq name index addrType lq@ListRequest{..} = do
$(logInfo) $ format $ unlines
[ "AddrTxs"
, " Account name : " ++ unpack name
, " Address index: " ++ show index
, " Address type : " ++ show addrType
, " Offset : " ++ show listOffset
, " Limit : " ++ show listLimit
, " Reversed : " ++ show listReverse
]
(res, cnt, bb) <- runDB $ do
accE <- getAccount name
addrE <- getAddress accE addrType index
bb <- walletBestBlock
(res, cnt) <- addrTxs accE addrE lq
return (res, cnt, bb)
return $ Just $ toJSON $ ListResult (map (f bb) res) cnt
where
f bb = toJsonTx name (Just bb)
createTxReq :: ( MonadLoggerIO m, MonadBaseControl IO m, MonadBase IO m
, MonadThrow m, MonadResource m
)
=> AccountName
-> CreateTx
-> Handler m (Maybe Value)
createTxReq name (CreateTx rs fee minconf rcptFee sign masterM) = do
$(logInfo) $ format $ unlines
[ "CreateTx"
, " Account name: " ++ unpack name
, " Recipients : " ++ show (map (first addrToBase58) rs)
, " Fee : " ++ show fee
, " Minconf : " ++ show minconf
, " Rcpt. Fee : " ++ show rcptFee
, " Sign : " ++ show sign
]
notif <- asks handlerNotifChan
(bb, txRes, newAddrs) <- runDB $ do
accE <- getAccount name
bb <- walletBestBlock
(txRes, newAddrs) <- createWalletTx
accE (Just notif) masterM rs fee minconf rcptFee sign
return (bb, txRes, newAddrs)
whenOnline $ do
-- Update the bloom filter
unless (null newAddrs) updateNodeFilter
-- If the transaction is pending, broadcast it to the network
when (walletTxConfidence txRes == TxPending) $
runNode $ broadcastTxs [walletTxHash txRes]
return $ Just $ toJSON $ toJsonTx name (Just bb) txRes
importTxReq :: ( MonadLoggerIO m, MonadBaseControl IO m, MonadBase IO m
, MonadThrow m, MonadResource m
)
=> AccountName -> Tx -> Handler m (Maybe Value)
importTxReq name tx = do
$(logInfo) $ format $ unlines
[ "ImportTx"
, " Account name: " ++ unpack name
, " TxId : " ++ cs (txHashToHex (txHash tx))
]
notif <- asks handlerNotifChan
(bb, txRes, newAddrs) <- runDB $ do
Entity ai _ <- getAccount name
bb <- walletBestBlock
(res, newAddrs) <- importTx tx (Just notif) ai
case filter ((== ai) . walletTxAccount) res of
(txRes:_) -> return (bb, txRes, newAddrs)
_ -> throwM $ WalletException "Could not import the transaction"
whenOnline $ do
-- Update the bloom filter
unless (null newAddrs) updateNodeFilter
-- If the transaction is pending, broadcast it to the network
when (walletTxConfidence txRes == TxPending) $
runNode $ broadcastTxs [walletTxHash txRes]
return $ Just $ toJSON $ toJsonTx name (Just bb) txRes
signTxReq :: ( MonadLoggerIO m, MonadBaseControl IO m, MonadBase IO m
, MonadThrow m, MonadResource m
)
=> AccountName -> SignTx -> Handler m (Maybe Value)
signTxReq name (SignTx txid masterM) = do
$(logInfo) $ format $ unlines
[ "SignTx"
, " Account name: " ++ unpack name
, " TxId : " ++ cs (txHashToHex txid)
]
notif <- asks handlerNotifChan
(bb, txRes, newAddrs) <- runDB $ do
accE@(Entity ai _) <- getAccount name
bb <- walletBestBlock
(res, newAddrs) <- signAccountTx accE (Just notif) masterM txid
case filter ((== ai) . walletTxAccount) res of
(txRes:_) -> return (bb, txRes, newAddrs)
_ -> throwM $ WalletException "Could not sign the transaction"
whenOnline $ do
-- Update the bloom filter
unless (null newAddrs) updateNodeFilter
-- If the transaction is pending, broadcast it to the network
when (walletTxConfidence txRes == TxPending) $
runNode $ broadcastTxs [walletTxHash txRes]
return $ Just $ toJSON $ toJsonTx name (Just bb) txRes
txReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName -> TxHash -> Handler m (Maybe Value)
txReq name txid = do
$(logInfo) $ format $ unlines
[ "Tx"
, " Account name: " ++ unpack name
, " TxId : " ++ cs (txHashToHex txid)
]
(res, bb) <- runDB $ do
Entity ai _ <- getAccount name
bb <- walletBestBlock
res <- getAccountTx ai txid
return (res, bb)
return $ Just $ toJSON $ toJsonTx name (Just bb) res
-- TODO: This should be limited to a single account
deleteTxReq :: (MonadLoggerIO m, MonadThrow m, MonadBaseControl IO m)
=> TxHash -> Handler m (Maybe Value)
deleteTxReq txid = do
$(logInfo) $ format $ unlines
[ "DeleteTx"
, " TxId: " ++ cs (txHashToHex txid)
]
runDB $ deleteTx txid
return Nothing
offlineTxReq :: ( MonadLoggerIO m, MonadBaseControl IO m
, MonadBase IO m, MonadThrow m, MonadResource m
)
=> AccountName -> TxHash -> Handler m (Maybe Value)
offlineTxReq accountName txid = do
$(logInfo) $ format $ unlines
[ "OfflineTx"
, " Account name: " ++ unpack accountName
, " TxId : " ++ cs (txHashToHex txid)
]
(dat, _) <- runDB $ do
Entity ai _ <- getAccount accountName
getOfflineTxData ai txid
return $ Just $ toJSON dat
signOfflineTxReq :: ( MonadLoggerIO m, MonadBaseControl IO m
, MonadBase IO m, MonadThrow m, MonadResource m
)
=> AccountName
-> Maybe XPrvKey
-> Tx
-> [CoinSignData]
-> Handler m (Maybe Value)
signOfflineTxReq accountName masterM tx signData = do
$(logInfo) $ format $ unlines
[ "SignOfflineTx"
, " Account name: " ++ unpack accountName
, " TxId : " ++ cs (txHashToHex (txHash tx))
]
Entity _ acc <- runDB $ getAccount accountName
let signedTx = signOfflineTx acc masterM tx signData
complete = verifyStdTx signedTx $ map toDat signData
toDat CoinSignData{..} = (coinSignScriptOutput, coinSignOutPoint)
return $ Just $ toJSON $ TxCompleteRes signedTx complete
balanceReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> AccountName -> Word32 -> Bool
-> Handler m (Maybe Value)
balanceReq name minconf offline = do
$(logInfo) $ format $ unlines
[ "Balance"
, " Account name: " ++ unpack name
, " Minconf : " ++ show minconf
, " Offline : " ++ show offline
]
bal <- runDB $ do
Entity ai _ <- getAccount name
accountBalance ai minconf offline
return $ Just $ toJSON bal
nodeActionReq :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)
=> NodeAction -> Handler m (Maybe Value)
nodeActionReq action = case action of
NodeActionRescan tM -> do
t <- case tM of
Just t -> return $ adjustFCTime t
Nothing -> do
timeM <- runDB firstAddrTime
maybe err (return . adjustFCTime) timeM
$(logInfo) $ format $ unlines
[ "Node Rescan"
, " Timestamp: " ++ show t
]
whenOnline $ do
runDB resetRescan
runNode $ atomicallyNodeT $ rescanTs t
return $ Just $ toJSON $ RescanRes t
NodeActionStatus -> do
$(logInfo) $ format "Node Status"
status <- runNode $ atomicallyNodeT nodeStatus
return $ Just $ toJSON status
where
err = throwM $ WalletException
"No keys have been generated in the wallet"
syncReq :: (MonadThrow m, MonadLoggerIO m, MonadBaseControl IO m)
=> AccountName
-> Either BlockHeight BlockHash
-> ListRequest
-> Handler m (Maybe Value)
syncReq acc blockE lq@ListRequest{..} = runDB $ do
$(logInfo) $ format $ unlines
[ "Sync"
, " Account name: " ++ cs acc
, " Block : " ++ showBlock
, " Offset : " ++ show listOffset
, " Limit : " ++ show listLimit
, " Reversed : " ++ show listReverse
]
ListResult nodes cnt <- mainChain blockE lq
case nodes of
[] -> return $ Just $ toJSON $ ListResult ([] :: [()]) cnt
b:_ -> do
Entity ai _ <- getAccount acc
ts <- accTxsFromBlock ai (nodeBlockHeight b)
(fromIntegral $ length nodes)
let bts = blockTxs nodes ts
return $ Just $ toJSON $ ListResult (map f bts) cnt
where
f (block, txs') = JsonSyncBlock
{ jsonSyncBlockHash = nodeHash block
, jsonSyncBlockHeight = nodeBlockHeight block
, jsonSyncBlockPrev = nodePrev block
, jsonSyncBlockTxs = map (toJsonTx acc Nothing) txs'
}
showBlock = case blockE of
Left e -> show e
Right b -> cs $ blockHashToHex b
blockInfoReq :: (MonadThrow m, MonadLoggerIO m, MonadBaseControl IO m)
=> [BlockHash] -> Handler m (Maybe Value)
blockInfoReq headerLst = do
$(logInfo) $ format "Received BlockInfo request"
lstMaybeBlk <- forM headerLst (runNode . runSqlNodeT . getBlockByHash)
return $ toJSON <$> Just (handleRes lstMaybeBlk)
where
handleRes :: [Maybe NodeBlock] -> [BlockInfo]
handleRes = map fromNodeBlock . catMaybes
stopServerReq :: MonadLoggerIO m => Handler m (Maybe Value)
stopServerReq = do
$(logInfo) $ format "Received StopServer request"
return Nothing
{- Helpers -}
whenOnline :: Monad m => Handler m () -> Handler m ()
whenOnline handler = do
mode <- configMode `liftM` asks handlerConfig
when (mode == SPVOnline) handler
updateNodeFilter
:: (MonadBaseControl IO m, MonadLoggerIO m, MonadThrow m)
=> Handler m ()
updateNodeFilter = do
$(logInfo) $ format "Sending a new bloom filter"
(bloom, elems, _) <- runDB getBloomFilter
runNode $ atomicallyNodeT $ sendBloomFilter bloom elems
adjustFCTime :: Timestamp -> Timestamp
adjustFCTime ts = fromInteger $ max 0 $ toInteger ts - 86400 * 7
format :: String -> Text
format str = pack $ "[ZeroMQ] " ++ str

View File

@ -1,202 +0,0 @@
module Network.Haskoin.Wallet.Settings
( SPVMode(..)
, OutputFormat(..)
, Config(..)
) where
import Control.Monad (mzero)
import Control.Exception (throw)
import Control.Monad.Logger (LogLevel(..))
import Data.Default (Default, def)
import Data.FileEmbed (embedFile)
import Data.Yaml (decodeEither')
import Data.Word (Word8, Word32, Word64)
import Data.HashMap.Strict (HashMap, unionWith)
import Data.String.Conversions (cs)
import Data.ByteString (ByteString)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Data.Aeson
( Value(..), FromJSON, ToJSON
, parseJSON, toJSON, withObject
, (.:)
)
import Network.Haskoin.Crypto
import Network.Haskoin.Wallet.Database
import Network.Haskoin.Wallet.Types
import Data.Restricted (Restricted, Div5)
import System.ZMQ4 (toRestricted)
data SPVMode = SPVOnline | SPVOffline
deriving (Eq, Show, Read)
newtype LogLevelJSON = LogLevelJSON LogLevel
deriving (Eq, Show, Read)
data OutputFormat
= OutputNormal
| OutputJSON
| OutputYAML
data Config = Config
{ configCount :: !Word32
-- ^ Output size of commands
, configMinConf :: !Word32
-- ^ Minimum number of confirmations
, configSignTx :: !Bool
-- ^ Sign transactions
, configFee :: !Word64
-- ^ Fee to pay per 1000 bytes when creating new transactions
, configRcptFee :: !Bool
-- ^ Recipient pays fee (dangerous, no config file setting)
, configAddrType :: !AddressType
-- ^ Return internal instead of external addresses
, configDisplayPubKeys :: !Bool
-- ^ Display public keys instead of addresses
, configOffline :: !Bool
-- ^ Display the balance including offline transactions
, configEntropy :: !Word8
-- ^ Entropy in bytes to use when generating a mnemonic (between 16 and 32)
, configReversePaging :: !Bool
-- ^ Use reverse paging for displaying addresses and transactions
, configDerivIndex :: !KeyIndex
-- ^ Derivation path when creating account
, configFormat :: !OutputFormat
-- ^ How to format the command-line results
, configConnect :: !String
-- ^ ZeroMQ socket to connect to (location of the server)
, configConnectNotif :: !String
-- ^ ZeroMQ socket to connect for notifications
, configDetach :: !Bool
-- ^ Detach server when launched from command-line
, configFile :: !FilePath
-- ^ Configuration file
, configTestnet :: !Bool
-- ^ Use Testnet3 network
, configDir :: !FilePath
-- ^ Working directory
, configBind :: !String
-- ^ Bind address for the ZeroMQ socket
, configBindNotif :: !String
-- ^ Bind address for ZeroMQ notifications
, configBTCNodes :: !(HashMap Text [BTCNode])
-- ^ Trusted Bitcoin full nodes to connect to
, configMode :: !SPVMode
-- ^ Operation mode of the SPV node.
, configBloomFP :: !Double
-- ^ False positive rate for the bloom filter.
, configDatabase :: !(HashMap Text DatabaseConfType)
-- ^ Database configuration
, configLogFile :: !FilePath
-- ^ Log file
, configPidFile :: !FilePath
-- ^ PID File
, configLogLevel :: !LogLevel
-- ^ Log level
, configVerbose :: !Bool
-- ^ Verbose
, configServerKey :: !(Maybe (Restricted Div5 ByteString))
-- ^ Server key for authentication and encryption (server config)
, configServerKeyPub :: !(Maybe (Restricted Div5 ByteString))
-- ^ Server public key for authentication and encryption (client config)
, configClientKey :: !(Maybe (Restricted Div5 ByteString))
-- ^ Client key for authentication and encryption (client config)
, configClientKeyPub :: !(Maybe (Restricted Div5 ByteString))
-- ^ Client public key for authentication and encryption
-- (client + server config)
}
configBS :: ByteString
configBS = $(embedFile "config/config.yml")
instance ToJSON OutputFormat where
toJSON OutputNormal = String "normal"
toJSON OutputJSON = String "json"
toJSON OutputYAML = String "yaml"
instance FromJSON OutputFormat where
parseJSON (String "normal") = return OutputNormal
parseJSON (String "json") = return OutputJSON
parseJSON (String "yaml") = return OutputYAML
parseJSON _ = mzero
instance ToJSON SPVMode where
toJSON SPVOnline = String "online"
toJSON SPVOffline = String "offline"
instance FromJSON SPVMode where
parseJSON (String "online") = return SPVOnline
parseJSON (String "offline") = return SPVOffline
parseJSON _ = mzero
instance ToJSON LogLevelJSON where
toJSON (LogLevelJSON LevelDebug) = String "debug"
toJSON (LogLevelJSON LevelInfo) = String "info"
toJSON (LogLevelJSON LevelWarn) = String "warn"
toJSON (LogLevelJSON LevelError) = String "error"
toJSON (LogLevelJSON (LevelOther t)) = String t
instance FromJSON LogLevelJSON where
parseJSON (String "debug") = return $ LogLevelJSON LevelDebug
parseJSON (String "info") = return $ LogLevelJSON LevelInfo
parseJSON (String "warn") = return $ LogLevelJSON LevelWarn
parseJSON (String "error") = return $ LogLevelJSON LevelError
parseJSON (String x) = return $ LogLevelJSON (LevelOther x)
parseJSON _ = mzero
instance Default Config where
def = either throw id $ decodeEither' "{}"
instance FromJSON Config where
parseJSON = withObject "config" $ \o' -> do
let defValue = either throw id $ decodeEither' configBS
(Object o) = mergeValues defValue (Object o')
configDerivIndex = 0
configFile <- o .: "config-file"
configRcptFee <- o .: "recipient-fee"
configCount <- o .: "output-size"
configMinConf <- o .: "minimum-confirmations"
configSignTx <- o .: "sign-transactions"
configFee <- o .: "transaction-fee"
configAddrType <- o .: "address-type"
configDisplayPubKeys <- o .: "display-pubkeys"
configOffline <- o .: "offline"
configEntropy <- o .: "seed-entropy"
configReversePaging <- o .: "reverse-paging"
configFormat <- o .: "display-format"
configConnect <- o .: "connect-uri"
configConnectNotif <- o .: "connect-uri-notif"
configDetach <- o .: "detach-server"
configTestnet <- o .: "use-testnet"
configDir <- o .: "work-dir"
configBind <- o .: "bind-socket"
configBindNotif <- o .: "bind-socket-notif"
configBTCNodes <- o .: "bitcoin-full-nodes"
configMode <- o .: "server-mode"
configBloomFP <- o .: "bloom-false-positive"
configLogFile <- o .: "log-file"
configPidFile <- o .: "pid-file"
LogLevelJSON configLogLevel <- o .: "log-level"
configVerbose <- o .: "verbose"
configDatabase <- o .: "database"
configServerKey <- getKey o "server-key"
configServerKeyPub <- getKey o "server-key-public"
configClientKey <- getKey o "client-key"
configClientKeyPub <- getKey o "client-key-public"
return Config {..}
where
getKey o i = o .: i >>= \kM ->
case kM of
Nothing -> return Nothing
Just k ->
case toRestricted $ encodeUtf8 k of
Just k' -> return $ Just k'
Nothing -> fail $ "Invalid " ++ cs k
mergeValues :: Value -> Value -> Value
mergeValues (Object d) (Object c) = Object (unionWith mergeValues d c)
mergeValues _ c = c

File diff suppressed because it is too large Load Diff

View File

@ -1,763 +0,0 @@
{-# LANGUAGE DeriveGeneric #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Network.Haskoin.Wallet.Types
( AccountName
-- JSON Types
, JsonAccount(..)
, JsonAddr(..)
, JsonCoin(..)
, JsonTx(..)
-- Request Types
, WalletRequest(..)
, ListRequest(..)
, NewAccount(..)
, OfflineTxData(..)
, CoinSignData(..)
, CreateTx(..)
, SignTx(..)
, NodeAction(..)
, AccountType(..)
, AddressType(..)
, addrTypeIndex
, TxType(..)
, TxConfidence(..)
, AddressInfo(..)
, BalanceInfo(..)
-- Response Types
, WalletResponse(..)
, TxCompleteRes(..)
, ListResult(..)
, RescanRes(..)
, JsonSyncBlock(..)
, JsonBlock(..)
, Notif(..)
, BlockInfo(..)
-- Helper Types
, WalletException(..)
, BTCNode(..)
-- *Helpers
, splitSelect
, splitUpdate
, splitDelete
, splitInsertMany_
, join2
, limitOffset
) where
import Control.DeepSeq (NFData (..))
import Control.Exception (Exception)
import Control.Monad
import Control.Monad.Trans (MonadIO)
import Data.Aeson
import Data.Aeson.TH
import qualified Data.ByteString as BS
import Data.Char (toLower)
import Data.Int (Int64)
import Data.List.Split (chunksOf)
import Data.Maybe (maybeToList)
import qualified Data.Serialize as S
import Data.String (fromString)
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Time (UTCTime)
import Data.Typeable (Typeable)
import Data.Word (Word32, Word64, Word8)
import qualified Database.Esqueleto as E
import Database.Esqueleto.Internal.Sql (SqlSelect)
import qualified Database.Persist as P
import Database.Persist.Class
import Database.Persist.Sql
import GHC.Generics
import Network.Haskoin.Block
import Network.Haskoin.Crypto
import Network.Haskoin.Network
import Network.Haskoin.Script
import Network.Haskoin.Transaction
import Network.Haskoin.Util
import Network.Haskoin.Wallet.Database
import Network.Haskoin.Wallet.Types.BlockInfo
type AccountName = Text
-- TODO: Add NFData instances for all those types
{- Request Types -}
data TxType
= TxIncoming
| TxOutgoing
| TxSelf
deriving (Eq, Show, Read)
instance NFData TxType where
rnf x = x `seq` ()
$(deriveJSON (dropSumLabels 2 0 "") ''TxType)
data TxConfidence
= TxOffline
| TxDead
| TxPending
| TxBuilding
deriving (Eq, Show, Read)
instance NFData TxConfidence where
rnf x = x `seq` ()
$(deriveJSON (dropSumLabels 2 0 "") ''TxConfidence)
data AddressInfo = AddressInfo
{ addressInfoAddress :: !Address
, addressInfoValue :: !(Maybe Word64)
, addressInfoIsLocal :: !Bool
}
deriving (Eq, Show, Read, Generic)
instance S.Serialize AddressInfo
$(deriveJSON (dropFieldLabel 11) ''AddressInfo)
instance NFData AddressInfo where
rnf AddressInfo{..} =
rnf addressInfoAddress `seq`
rnf addressInfoValue `seq`
rnf addressInfoIsLocal
data BalanceInfo = BalanceInfo
{ balanceInfoInBalance :: !Word64
, balanceInfoOutBalance :: !Word64
, balanceInfoCoins :: !Int
, balanceInfoSpentCoins :: !Int
}
deriving (Eq, Show, Read)
$(deriveJSON (dropFieldLabel 11) ''BalanceInfo)
instance NFData BalanceInfo where
rnf BalanceInfo{..} =
rnf balanceInfoInBalance `seq`
rnf balanceInfoOutBalance `seq`
rnf balanceInfoCoins `seq`
rnf balanceInfoSpentCoins
data AccountType
= AccountRegular
| AccountMultisig
{ accountTypeRequiredSigs :: !Int
, accountTypeTotalKeys :: !Int
}
deriving (Eq, Show, Read)
instance NFData AccountType where
rnf t = case t of
AccountRegular -> ()
AccountMultisig m n -> rnf m `seq` rnf n
instance ToJSON AccountType where
toJSON accType = case accType of
AccountRegular -> object
[ "type" .= String "regular" ]
AccountMultisig m n -> object
[ "type" .= String "multisig"
, "requiredsigs" .= m
, "totalkeys" .= n
]
instance FromJSON AccountType where
parseJSON = withObject "AccountType" $ \o ->
o .: "type" >>= \t -> case (t :: Text) of
"regular" -> return AccountRegular
"multisig" -> AccountMultisig <$> o .: "requiredsigs"
<*> o .: "totalkeys"
_ -> mzero
data NewAccount = NewAccount
{ newAccountName :: !AccountName
, newAccountType :: !AccountType
, newAccountMnemonic :: !(Maybe Text)
, newAccountPassword :: !(Maybe Text)
, newAccountEntropy :: !(Maybe Word8)
, newAccountMaster :: !(Maybe XPrvKey)
, newAccountDeriv :: !(Maybe KeyIndex)
, newAccountKeys :: ![XPubKey]
, newAccountReadOnly :: !Bool
}
deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 10) ''NewAccount)
data ListRequest = ListRequest
{ listOffset :: !Word32
, listLimit :: !Word32
, listReverse :: !Bool
}
deriving (Eq, Show, Read)
$(deriveJSON (dropFieldLabel 4) ''ListRequest)
data CoinSignData = CoinSignData
{ coinSignOutPoint :: !OutPoint
, coinSignScriptOutput :: !ScriptOutput
, coinSignDeriv :: !SoftPath
}
deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 8) ''CoinSignData)
instance S.Serialize CoinSignData where
get = do
p <- S.get
s <- readBS >>= \bs -> case decodeOutputBS bs of
Right s -> return s
_ -> error "Invalid ScriptOutput in CoinSignData"
S.get >>= \dM -> case toSoft (dM :: DerivPath) of
Just d -> return $ CoinSignData p s d
_ -> error "Invalid derivation in CoinSignData"
where
readBS = S.get >>= \(VarInt l) -> S.getByteString $ fromIntegral l
put (CoinSignData p s d) = do
S.put p
writeBS $ encodeOutputBS s
S.put $ toGeneric d
where
writeBS bs = do
S.put $ VarInt $ fromIntegral $ BS.length bs
S.putByteString bs
data OfflineTxData = OfflineTxData
{ offlineTxDataTx :: !Tx
, offlineTxDataCoinData :: ![CoinSignData]
}
$(deriveJSON (dropFieldLabel 13) ''OfflineTxData)
instance S.Serialize OfflineTxData where
get = OfflineTxData <$> S.get <*> (replicateList =<< S.get)
where
replicateList (VarInt c) = replicateM (fromIntegral c) S.get
put (OfflineTxData t ds) = do
S.put t
S.put $ VarInt $ fromIntegral $ length ds
forM_ ds S.put
data CreateTx = CreateTx
{ createTxRecipients :: ![(Address, Word64)]
, createTxFee :: !Word64
, createTxMinConf :: !Word32
, createTxRcptFee :: !Bool
, createTxSign :: !Bool
, createTxSignKey :: !(Maybe XPrvKey)
} deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 8) ''CreateTx)
data SignTx = SignTx
{ signTxTxHash :: !TxHash
, signTxSignKey :: !(Maybe XPrvKey)
} deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 6) ''SignTx)
data NodeAction
= NodeActionRescan { nodeActionTimestamp :: !(Maybe Word32) }
| NodeActionStatus
deriving (Eq, Show, Read)
instance ToJSON NodeAction where
toJSON na = case na of
NodeActionRescan tM -> object $
("type" .= String "rescan") : (("timestamp" .=) <$> maybeToList tM)
NodeActionStatus -> object [ "type" .= String "status" ]
instance FromJSON NodeAction where
parseJSON = withObject "NodeAction" $ \o -> do
String t <- o .: "type"
case t of
"rescan" -> NodeActionRescan <$> o .:? "timestamp"
"status" -> return NodeActionStatus
_ -> mzero
data AddressType
= AddressInternal
| AddressExternal
deriving (Eq, Show, Read)
$(deriveJSON (dropSumLabels 7 0 "") ''AddressType)
instance NFData AddressType where
rnf x = x `seq` ()
addrTypeIndex :: AddressType -> KeyIndex
addrTypeIndex AddressExternal = 0
addrTypeIndex AddressInternal = 1
data WalletRequest
= AccountReq !AccountName
| AccountsReq !ListRequest
| NewAccountReq !NewAccount
| RenameAccountReq !AccountName !AccountName
| AddPubKeysReq !AccountName ![XPubKey]
| SetAccountGapReq !AccountName !Word32
| AddrsReq !AccountName !AddressType !Word32 !Bool !ListRequest
| UnusedAddrsReq !AccountName !AddressType !ListRequest
| AddressReq !AccountName !KeyIndex !AddressType !Word32 !Bool
| PubKeyIndexReq !AccountName !PubKeyC !AddressType
| SetAddrLabelReq !AccountName !KeyIndex !AddressType !Text
| GenerateAddrsReq !AccountName !KeyIndex !AddressType
| TxsReq !AccountName !ListRequest
| PendingTxsReq !AccountName !ListRequest
| DeadTxsReq !AccountName !ListRequest
| AddrTxsReq !AccountName !KeyIndex !AddressType !ListRequest
| CreateTxReq !AccountName !CreateTx
| ImportTxReq !AccountName !Tx
| SignTxReq !AccountName !SignTx
| TxReq !AccountName !TxHash
| DeleteTxReq !TxHash
| OfflineTxReq !AccountName !TxHash
| SignOfflineTxReq !AccountName !(Maybe XPrvKey) !Tx ![CoinSignData]
| BalanceReq !AccountName !Word32 !Bool
| NodeActionReq !NodeAction
| SyncReq !AccountName !BlockHash !ListRequest
| SyncHeightReq !AccountName !BlockHeight !ListRequest
| BlockInfoReq ![BlockHash]
| StopServerReq
deriving (Show, Eq)
-- TODO: Set omitEmptyContents on aeson-0.9
$(deriveJSON
defaultOptions
{ constructorTagModifier = map toLower . init
, sumEncoding = defaultTaggedObject
{ tagFieldName = "method"
, contentsFieldName = "request"
}
}
''WalletRequest
)
{- JSON Types -}
data JsonAccount = JsonAccount
{ jsonAccountName :: !Text
, jsonAccountType :: !AccountType
, jsonAccountMaster :: !(Maybe XPrvKey)
, jsonAccountMnemonic :: !(Maybe Text)
, jsonAccountKeys :: ![XPubKey]
, jsonAccountGap :: !Word32
, jsonAccountCreated :: !UTCTime
}
deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 11) ''JsonAccount)
data JsonAddr = JsonAddr
{ jsonAddrAddress :: !Address
, jsonAddrIndex :: !KeyIndex
, jsonAddrType :: !AddressType
, jsonAddrLabel :: !Text
, jsonAddrRedeem :: !(Maybe ScriptOutput)
, jsonAddrKey :: !(Maybe PubKeyC)
, jsonAddrCreated :: !UTCTime
-- Optional Balance
, jsonAddrBalance :: !(Maybe BalanceInfo)
}
deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 8) ''JsonAddr)
data JsonTx = JsonTx
{ jsonTxHash :: !TxHash
, jsonTxNosigHash :: !TxHash
, jsonTxType :: !TxType
, jsonTxInValue :: !Word64
, jsonTxOutValue :: !Word64
, jsonTxValue :: !Int64
, jsonTxInputs :: ![AddressInfo]
, jsonTxOutputs :: ![AddressInfo]
, jsonTxChange :: ![AddressInfo]
, jsonTxTx :: !Tx
, jsonTxIsCoinbase :: !Bool
, jsonTxConfidence :: !TxConfidence
, jsonTxConfirmedBy :: !(Maybe BlockHash)
, jsonTxConfirmedHeight :: !(Maybe Word32)
, jsonTxConfirmedDate :: !(Maybe Word32)
, jsonTxCreated :: !UTCTime
, jsonTxAccount :: !AccountName
-- Optional confirmation
, jsonTxConfirmations :: !(Maybe Word32)
, jsonTxBestBlock :: !(Maybe BlockHash)
, jsonTxBestBlockHeight :: !(Maybe BlockHeight)
}
deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 6) ''JsonTx)
data JsonCoin = JsonCoin
{ jsonCoinHash :: !TxHash
, jsonCoinPos :: !Word32
, jsonCoinValue :: !Word64
, jsonCoinScript :: !ScriptOutput
, jsonCoinCreated :: !UTCTime
-- Optional Tx
, jsonCoinTx :: !(Maybe JsonTx)
-- Optional Address
, jsonCoinAddress :: !(Maybe JsonAddr)
-- Optional spender
, jsonCoinSpendingTx :: !(Maybe JsonTx)
}
deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 8) ''JsonCoin)
{- Response Types -}
data TxCompleteRes = TxCompleteRes
{ txCompleteTx :: !Tx
, txCompleteComplete :: !Bool
} deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 10) ''TxCompleteRes)
data ListResult a = ListResult
{ listResultItems :: ![a]
, listResultTotal :: !Word32
}
$(deriveJSON (dropFieldLabel 10) ''ListResult)
data RescanRes = RescanRes { rescanTimestamp :: !Word32 }
deriving (Eq, Show, Read)
$(deriveJSON (dropFieldLabel 6) ''RescanRes)
data WalletResponse a
= ResponseError { responseError :: !Text }
| ResponseValid { responseResult :: !(Maybe a) }
deriving (Eq, Show)
$(deriveJSON (dropSumLabels 8 8 "status") ''WalletResponse)
data JsonSyncBlock = JsonSyncBlock
{ jsonSyncBlockHash :: !BlockHash
, jsonSyncBlockHeight :: !BlockHeight
, jsonSyncBlockPrev :: !BlockHash
, jsonSyncBlockTxs :: ![JsonTx]
} deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 13) ''JsonSyncBlock)
data JsonBlock = JsonBlock
{ jsonBlockHash :: !BlockHash
, jsonBlockHeight :: !BlockHeight
, jsonBlockPrev :: !BlockHash
} deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 9) ''JsonBlock)
data Notif
= NotifBlock !JsonBlock
| NotifTx !JsonTx
deriving (Eq, Show)
$(deriveJSON (dropSumLabels 5 5 "type") ''Notif)
{- Helper Types -}
data WalletException = WalletException !String
deriving (Eq, Read, Show, Typeable)
instance Exception WalletException
data BTCNode = BTCNode { btcNodeHost :: !String, btcNodePort :: !Int }
deriving (Eq, Read, Show)
$(deriveJSON (dropFieldLabel 7) ''BTCNode)
{- Persistent Instances -}
instance PersistField XPrvKey where
toPersistValue = PersistText . cs . xPrvExport
fromPersistValue (PersistText txt) =
maybeToEither "Invalid Persistent XPrvKey" $ xPrvImport $ cs txt
fromPersistValue (PersistByteString bs) =
maybeToEither "Invalid Persistent XPrvKey" $ xPrvImport bs
fromPersistValue _ = Left "Invalid Persistent XPrvKey"
instance PersistFieldSql XPrvKey where
sqlType _ = SqlString
instance PersistField [XPubKey] where
toPersistValue = PersistText . cs . encode
fromPersistValue (PersistText txt) =
maybeToEither "Invalid Persistent XPubKey" $ decodeStrict' $ cs txt
fromPersistValue (PersistByteString bs) =
maybeToEither "Invalid Persistent XPubKey" $ decodeStrict' bs
fromPersistValue _ = Left "Invalid Persistent XPubKey"
instance PersistFieldSql [XPubKey] where
sqlType _ = SqlString
instance PersistField DerivPath where
toPersistValue = PersistText . cs . pathToStr
fromPersistValue (PersistText txt) = maybeToEither
"Invalid Persistent DerivPath" . fmap getParsedPath .
parsePath . cs $ txt
fromPersistValue (PersistByteString bs) = maybeToEither
"Invalid Persistent DerivPath" . fmap getParsedPath .
parsePath . cs $ bs
fromPersistValue _ = Left "Invalid Persistent DerivPath"
instance PersistFieldSql DerivPath where
sqlType _ = SqlString
instance PersistField HardPath where
toPersistValue = PersistText . cs . pathToStr
fromPersistValue (PersistText txt) = maybeToEither
"Invalid Persistent HardPath" $ parseHard $ cs txt
fromPersistValue (PersistByteString bs) = maybeToEither
"Invalid Persistent HardPath" $ parseHard $ cs bs
fromPersistValue _ = Left "Invalid Persistent HardPath"
instance PersistFieldSql HardPath where
sqlType _ = SqlString
instance PersistField SoftPath where
toPersistValue = PersistText . cs . pathToStr
fromPersistValue (PersistText txt) = maybeToEither
"Invalid Persistent SoftPath" $ parseSoft $ cs txt
fromPersistValue (PersistByteString bs) = maybeToEither
"Invalid Persistent SoftPath" $ parseSoft $ cs bs
fromPersistValue _ = Left "Invalid Persistent SoftPath"
instance PersistFieldSql SoftPath where
sqlType _ = SqlString
instance PersistField AccountType where
toPersistValue = PersistText . cs . encode
fromPersistValue (PersistText txt) = maybeToEither
"Invalid Persistent AccountType" $ decodeStrict' $ cs txt
fromPersistValue (PersistByteString bs) = maybeToEither
"Invalid Persistent AccountType" $ decodeStrict' bs
fromPersistValue _ = Left "Invalid Persistent AccountType"
instance PersistFieldSql AccountType where
sqlType _ = SqlString
instance PersistField AddressType where
toPersistValue ts = PersistBool $ case ts of
AddressExternal -> True
AddressInternal -> False
fromPersistValue (PersistBool b) = return $
if b then AddressExternal else AddressInternal
fromPersistValue (PersistInt64 i) = return $ case i of
0 -> AddressInternal
_ -> AddressExternal
fromPersistValue _ = Left "Invalid Persistent AddressType"
instance PersistFieldSql AddressType where
sqlType _ = SqlBool
instance PersistField TxType where
toPersistValue ts = PersistText $ case ts of
TxIncoming -> "incoming"
TxOutgoing -> "outgoing"
TxSelf -> "self"
fromPersistValue (PersistText txt) = case txt of
"incoming" -> return TxIncoming
"outgoing" -> return TxOutgoing
"self" -> return TxSelf
_ -> Left "Invalid Persistent TxType"
fromPersistValue (PersistByteString bs) = case bs of
"incoming" -> return TxIncoming
"outgoing" -> return TxOutgoing
"self" -> return TxSelf
_ -> Left "Invalid Persistent TxType"
fromPersistValue _ = Left "Invalid Persistent TxType"
instance PersistFieldSql TxType where
sqlType _ = SqlString
instance PersistField Address where
toPersistValue = PersistText . cs . addrToBase58
fromPersistValue (PersistText a) =
maybeToEither "Invalid Persistent Address" $ base58ToAddr $ cs a
fromPersistValue (PersistByteString a) =
maybeToEither "Invalid Persistent Address" $ base58ToAddr a
fromPersistValue _ = Left "Invalid Persistent Address"
instance PersistFieldSql Address where
sqlType _ = SqlString
instance PersistField BloomFilter where
toPersistValue = PersistByteString . S.encode
fromPersistValue (PersistByteString bs) =
case S.decode bs of
Right x -> Right x
Left e -> Left (fromString e)
fromPersistValue _ = Left "Invalid Persistent BloomFilter"
instance PersistFieldSql BloomFilter where
sqlType _ = SqlBlob
instance PersistField BlockHash where
toPersistValue = PersistText . cs . blockHashToHex
fromPersistValue (PersistText h) =
maybeToEither "Could not decode BlockHash" $ hexToBlockHash $ cs h
fromPersistValue (PersistByteString h) =
maybeToEither "Could not decode BlockHash" $ hexToBlockHash h
fromPersistValue _ = Left "Invalid Persistent BlockHash"
instance PersistFieldSql BlockHash where
sqlType _ = SqlString
instance PersistField TxHash where
toPersistValue = PersistText . cs . txHashToHex
fromPersistValue (PersistText h) =
maybeToEither "Invalid Persistent TxHash" $ hexToTxHash $ cs h
fromPersistValue (PersistByteString h) =
maybeToEither "Invalid Persistent TxHash" $ hexToTxHash h
fromPersistValue _ = Left "Invalid Persistent TxHash"
instance PersistFieldSql TxHash where
sqlType _ = SqlString
instance PersistField TxConfidence where
toPersistValue tc = PersistText $ case tc of
TxOffline -> "offline"
TxDead -> "dead"
TxPending -> "pending"
TxBuilding -> "building"
fromPersistValue (PersistText txt) = case txt of
"offline" -> return TxOffline
"dead" -> return TxDead
"pending" -> return TxPending
"building" -> return TxBuilding
_ -> Left "Invalid Persistent TxConfidence"
fromPersistValue (PersistByteString bs) = case bs of
"offline" -> return TxOffline
"dead" -> return TxDead
"pending" -> return TxPending
"building" -> return TxBuilding
_ -> Left "Invalid Persistent TxConfidence"
fromPersistValue _ = Left "Invalid Persistent TxConfidence"
instance PersistFieldSql TxConfidence where
sqlType _ = SqlString
instance PersistField Tx where
toPersistValue = PersistByteString . S.encode
fromPersistValue (PersistByteString bs) =
case S.decode bs of
Right x -> Right x
Left e -> Left (fromString e)
fromPersistValue _ = Left "Invalid Persistent Tx"
instance PersistFieldSql Tx where
sqlType _ = SqlOther "MEDIUMBLOB"
instance PersistField PubKeyC where
toPersistValue = PersistText . cs . encodeHex . S.encode
fromPersistValue (PersistText txt) =
case hex >>= S.decode of
Right x -> Right x
Left e -> Left (fromString e)
where
hex = maybeToEither "Could not decode hex" (decodeHex (cs txt))
fromPersistValue (PersistByteString bs) =
case hex >>= S.decode of
Right x -> Right x
Left e -> Left (fromString e)
where
hex = maybeToEither "Could not decode hex" (decodeHex bs)
fromPersistValue _ = Left "Invalid Persistent PubKeyC"
instance PersistFieldSql PubKeyC where
sqlType _ = SqlString
instance PersistField ScriptOutput where
toPersistValue = PersistByteString . encodeOutputBS
fromPersistValue (PersistByteString bs) =
case decodeOutputBS bs of
Right x -> Right x
Left e -> Left (fromString e)
fromPersistValue _ = Left "Invalid Persistent ScriptOutput"
instance PersistFieldSql ScriptOutput where
sqlType _ = SqlBlob
instance PersistField [AddressInfo] where
toPersistValue = PersistByteString . S.encode
fromPersistValue (PersistByteString bs) =
case S.decode bs of
Right x -> Right x
Left e -> Left (fromString e)
fromPersistValue _ = Left "Invalid Persistent AddressInfo"
instance PersistFieldSql [AddressInfo] where
sqlType _ = SqlOther "MEDIUMBLOB"
{- Helpers -}
-- Join AND expressions with OR conditions in a binary way
join2 :: [E.SqlExpr (E.Value Bool)] -> E.SqlExpr (E.Value Bool)
join2 xs = case xs of
[] -> E.val False
[x] -> x
_ -> let (ls,rs) = splitAt (length xs `div` 2) xs
in join2 ls E.||. join2 rs
splitSelect :: (SqlSelect a r, MonadIO m)
=> [t]
-> ([t] -> E.SqlQuery a)
-> E.SqlPersistT m [r]
splitSelect ts queryF =
fmap concat $ forM vals $ E.select . queryF
where
vals = chunksOf paramLimit ts
splitUpdate :: ( MonadIO m
, P.PersistEntity val
, P.PersistEntityBackend val ~ E.SqlBackend
)
=> [t]
-> ([t] -> E.SqlExpr (E.Entity val) -> E.SqlQuery ())
-> E.SqlPersistT m ()
splitUpdate ts updateF =
forM_ vals $ E.update . updateF
where
vals = chunksOf paramLimit ts
splitDelete :: MonadIO m => [t] -> ([t] -> E.SqlQuery ()) -> E.SqlPersistT m ()
splitDelete ts deleteF =
forM_ vals $ E.delete . deleteF
where
vals = chunksOf paramLimit ts
splitInsertMany_ :: ( MonadIO m
, P.PersistEntity val
, P.PersistEntityBackend val ~ E.SqlBackend
)
=> [val] -> E.SqlPersistT m ()
splitInsertMany_ = mapM_ P.insertMany_ . chunksOf paramLimit
limitOffset :: Word32 -> Word32 -> E.SqlQuery ()
limitOffset l o = do
when (l > 0) $ E.limit $ fromIntegral l
when (o > 0) $ E.offset $ fromIntegral o

View File

@ -1,77 +0,0 @@
{-# LANGUAGE TemplateHaskell #-}
module Network.Haskoin.Wallet.Types.BlockInfo
(
BlockInfo(..)
, JsonHash256
, fromNodeBlock
)
where
import Data.String.Conversions (cs, ConvertibleStrings(..))
import Data.Maybe (fromMaybe)
import Data.Aeson.TH (deriveJSON)
import Data.Word (Word32)
import Data.Time (UTCTime)
import Data.ByteString (ByteString)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Data.Aeson (FromJSON, ToJSON, Value (String),
parseJSON, toJSON, withText)
import Network.Haskoin.Block
import Network.Haskoin.Node.HeaderTree
import Network.Haskoin.Crypto
import Network.Haskoin.Util
newtype JsonHash256 = JsonHash256 { jsonGetHash256 :: Hash256 }
deriving (Eq, Show)
jsonHashToHex :: JsonHash256 -> ByteString
jsonHashToHex = blockHashToHex . BlockHash . jsonGetHash256
instance ToJSON JsonHash256 where
toJSON = String . cs . jsonHashToHex
instance FromJSON JsonHash256 where
parseJSON = withText "JsonHash256" $
return . JsonHash256 . getBlockHash <$>
fromMaybe (error "Invalid 256 bit hash") .
hexToBlockHash . cs
instance ConvertibleStrings JsonHash256 String where
convertString = cs . jsonHashToHex
data BlockInfo = BlockInfo
{ blockInfoHash :: !BlockHash
, blockInfoHeight :: !BlockHeight
, blockInfoVersion :: !Word32
, blockInfoTimestamp :: !UTCTime
, blockInfoPrevBlock :: !BlockHash
, blockInfoMerkleRoot :: !JsonHash256
, blockInfoBits :: !Word32
, blockInfoNonce :: !Word32
, blockInfoChain :: !Word32
, blockInfoChainWork :: !Double
} deriving (Eq, Show)
$(deriveJSON (dropFieldLabel 9) ''BlockInfo)
fromNodeBlock :: NodeBlock -> BlockInfo
fromNodeBlock nb =
BlockInfo
{ blockInfoHash = headerHash header
, blockInfoVersion = blockVersion header
, blockInfoPrevBlock = prevBlock header
, blockInfoNonce = bhNonce header
, blockInfoBits = blockBits header
, blockInfoMerkleRoot = JsonHash256 $ merkleRoot header
, blockInfoTimestamp = utcTimestamp
, blockInfoChainWork = nWork nb
, blockInfoHeight = nHeight nb
, blockInfoChain = nodeChain nb
}
where
header = nHeader nb
utcTimestamp = posixSecondsToUTCTime . realToFrac .
blockTimestamp $ header

View File

@ -1,15 +0,0 @@
module Main where
import Test.Framework (defaultMain)
import qualified Network.Haskoin.Wallet.Units (tests)
import qualified Network.Haskoin.Wallet.Tests (tests)
import Network.Haskoin.Constants
main :: IO ()
main = setProdnet >> defaultMain
( Network.Haskoin.Wallet.Tests.tests
++ Network.Haskoin.Wallet.Units.tests
)

View File

@ -1,21 +0,0 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Network.Haskoin.Wallet.Arbitrary where
import Test.QuickCheck (Arbitrary, arbitrary, oneof)
import Network.Haskoin.Test
import Network.Haskoin.Wallet
instance Arbitrary AccountType where
arbitrary = oneof
[ return AccountRegular
, do
(m, n) <- arbitraryMSParam
return $ AccountMultisig m n
]
instance Arbitrary NodeAction where
arbitrary = oneof [ NodeActionRescan <$> arbitrary
, return NodeActionStatus
]

View File

@ -1,23 +0,0 @@
module Network.Haskoin.Wallet.Tests (tests) where
import Test.Framework (Test, testGroup)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Data.Aeson (FromJSON, ToJSON, encode, decode)
import Data.HashMap.Strict (singleton)
import Network.Haskoin.Wallet.Arbitrary ()
import Network.Haskoin.Wallet
tests :: [Test]
tests =
[ testGroup "Serialize & de-serialize types to JSON"
[ testProperty "AccountType" (metaID :: AccountType -> Bool)
, testProperty "NodeAction" (metaID :: NodeAction -> Bool)
]
]
metaID :: (FromJSON a, ToJSON a, Eq a) => a -> Bool
metaID x = (decode . encode) (singleton ("object" :: String) x) ==
Just (singleton ("object" :: String) x)

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,4 @@
resolver: lts-10.1
flags: {}
packages:
- haskoin-core
- haskoin-node
- haskoin-wallet
extra-deps:
- daemons-0.2.1
- pbkdf-1.1.1.1

Some files were not shown because too many files have changed in this diff Show More