packages feed

haskoin-node 2.3.0 → 2.3.1

raw patch · 9 files changed

+1080/−57 lines, 9 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Haskoin.Node: [address] :: NodeConfig -> !NetworkAddress
+ Haskoin.Node: [address] :: PeerMgrConfig -> !NetworkAddress
- Haskoin.Node: [cf] :: NodeConfig -> !Maybe ColumnFamily
+ Haskoin.Node: [cf] :: ChainConfig -> !Maybe ColumnFamily
- Haskoin.Node: [connect] :: NodeConfig -> !SockAddr -> WithConnection
+ Haskoin.Node: [connect] :: PeerMgrConfig -> !SockAddr -> WithConnection
- Haskoin.Node: [db] :: NodeConfig -> !DB
+ Haskoin.Node: [db] :: ChainConfig -> !DB
- Haskoin.Node: [discover] :: NodeConfig -> !Bool
+ Haskoin.Node: [discover] :: PeerMgrConfig -> !Bool
- Haskoin.Node: [logger] :: NodeConfig -> !Logger
+ Haskoin.Node: [logger] :: ChainConfig -> !Logger
- Haskoin.Node: [maxPeerLife] :: NodeConfig -> !NominalDiffTime
+ Haskoin.Node: [maxPeerLife] :: PeerMgrConfig -> !NominalDiffTime
- Haskoin.Node: [maxPeers] :: NodeConfig -> !Int
+ Haskoin.Node: [maxPeers] :: PeerMgrConfig -> !Int
- Haskoin.Node: [net] :: NodeConfig -> !Network
+ Haskoin.Node: [net] :: ChainConfig -> !Network
- Haskoin.Node: [peers] :: NodeConfig -> ![String]
+ Haskoin.Node: [peers] :: PeerMgrConfig -> ![String]
- Haskoin.Node: [pub] :: NodeConfig -> !Publisher NodeEvent
+ Haskoin.Node: [pub] :: ChainConfig -> !Publisher ChainEvent
- Haskoin.Node: [timeout] :: NodeConfig -> !NominalDiffTime
+ Haskoin.Node: [timeout] :: ChainConfig -> !NominalDiffTime

Files

CHANGELOG.md view
@@ -4,6 +4,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [2.3.1] - 2026-11-07++### Changed++- Documentation: the README explains the purpose and design of the node, with+  installation steps, configuration, security notes and a tutorial, and the+  Haddock of every module and function says why it works the way it does.+- Build against nqe 0.6.9, rocksdb-haskell-jprupp 2.6.1, rocksdb-query 0.6.1+  and Stackage nightly-2026-11-07.+ ## [2.3.0] - 2026-11-06  ### Changed
README.md view
@@ -1,5 +1,505 @@ # Haskoin Node -Haskoin Node is a peer-to-peer library for Bitcoin and Bitcoin Cash. It uses a-RocksDB database to store blockchain headers and peers.+[![Hackage](https://img.shields.io/hackage/v/haskoin-node.svg)](https://hackage.haskell.org/package/haskoin-node)+[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jprupp/haskoin-node/blob/master/LICENSE) +Haskoin Node is a peer-to-peer networking library for Bitcoin (BTC) and Bitcoin+Cash (BCH), written in Haskell. It connects to the network, keeps a pool of+healthy peers, and syncs block headers into a [RocksDB](https://rocksdb.org)+database.++It does not download or validate blocks and transactions by itself. It+publishes everything that happens as events, and gives you the peers to ask for+the data you need. It is the networking layer of+[Haskoin Store](https://github.com/jprupp/haskoin-store).++- [API documentation](https://hackage.haskell.org/package/haskoin-node)+- [Changelog](https://github.com/jprupp/haskoin-node/blob/master/CHANGELOG.md)+- [Report a bug](https://github.com/jprupp/haskoin-node/issues)++## Contents++- [Why Haskoin Node](#why-haskoin-node)+- [Design principles](#design-principles)+- [Features](#features)+- [Installation](#installation)+- [Quick start](#quick-start)+- [Tutorial: a block watcher](#tutorial-a-block-watcher)+- [Concepts](#concepts)+- [Usage guide](#usage-guide)+- [Configuration](#configuration)+- [Module overview](#module-overview)+- [Security notes](#security-notes)+- [Development](#development)+- [Related projects](#related-projects)+- [License](#license)++## Why Haskoin Node++A program that wants Bitcoin data has two usual options. It can run a full node+such as Bitcoin Core next to it and talk to its RPC interface, which means+operating a second large piece of software. Or it can speak the peer-to-peer+protocol itself, which means solving the same problems every node solves:+finding peers, keeping good ones, dropping bad ones, parsing untrusted+messages safely, and following the header chain through reorganisations.++Haskoin Node solves those problems once, as a library. It gives a Haskell+program a direct, well-behaved presence on the network, and stops there: what+to download, what to validate and what to store is up to the program. It is+the networking layer of [Haskoin Store](https://github.com/jprupp/haskoin-store),+which indexes the whole Bitcoin and Bitcoin Cash block chains this way without+a full node.++## Design principles++- **Do one job.** Keep connected and keep the header chain current. Blocks,+  transactions, validation and storage of anything but headers belong to the+  application, because every application wants something different.+- **Headers are the source of truth.** Headers are small (80 bytes) and carry+  the proof of work. With them, the node knows which chain is real without+  downloading blocks, and can tell the application which blocks to fetch.+- **Tell, don't ask.** Everything the node learns becomes an event on a+  publisher. The application subscribes and reacts; the node never needs to know+  who is listening.+- **Peers are strangers.** Message sizes are checked before payloads are read,+  anything invalid ends the connection, relayed addresses are filtered, and a+  peer cannot push data faster than the node can handle it.+- **Prefer good peers, but keep moving.** Peers are ranked by measured latency+  so the fastest are used first, and every connection is eventually replaced so+  the node never depends on the same few peers forever.+- **Small, isolated components.** The peer manager, the header chain and each+  connection are separate [NQE](https://github.com/jprupp/nqe) processes with+  their own state. One misbehaving peer dies alone.++## Features++| Area            | What you get                                                                                   |+| --------------- | ---------------------------------------------------------------------------------------------- |+| Networks        | Every network of haskoin-core: Bitcoin and Bitcoin Cash mainnet, testnets and regtest           |+| Peer discovery  | DNS seeds and addresses relayed by peers, or a static list of peers                             |+| Peer management | Version handshake, ping-based latency, peers sorted by median ping, rotation after a maximum age |+| Header sync     | Headers downloaded from one peer at a time, connected to the chain with the most work, stored in RocksDB |+| Events          | Best header changes, sync completion, peer connections and every message received              |+| Data requests   | Fetch blocks and transactions from a peer, with a timeout                                       |+| Protection      | Strict parsing, per-command size limits, back-pressure, bounded address pool                    |++### What is not included++- No block or transaction validation, and no mempool. Haskoin Store does this+  on top of the events.+- No inbound connections: the node only connects out.+- No compact blocks, Bloom filters (BIP-37) or compact block filters (BIP-157).++## Installation++### System libraries++Haskoin Node needs [RocksDB](https://rocksdb.org) and+[libsecp256k1](https://github.com/bitcoin-core/secp256k1) 0.2.0 or later, with+`pkg-config` to find them.++```sh+# Debian and Ubuntu+sudo apt-get install librocksdb-dev libsecp256k1-dev pkg-config++# Fedora+sudo dnf install rocksdb-devel libsecp256k1-devel pkgconf++# macOS with Homebrew+brew install rocksdb secp256k1 pkg-config++# Nix+nix-shell -p rocksdb secp256k1 pkg-config+```++Some older distributions ship a libsecp256k1 below 0.2.0. See the+[haskoin-core installation notes](https://github.com/jprupp/haskoin-core#system-library)+for how to build it.++### Check the libraries are found++```sh+pkg-config --modversion libsecp256k1   # 0.2.0 or later+ls /usr/include/rocksdb/c.h            # or $(brew --prefix)/include/rocksdb/c.h+```++### Compiler++It needs GHC 9.2 or later, because it uses `OverloadedRecordDot`. It is built+and tested with the GHC of the Stackage nightly in `stack.yaml`.++### With Cabal++```cabal+build-depends:+    base+  , data-default+  , haskoin-core+  , haskoin-node+  , nqe+  , rocksdb-haskell-jprupp+```++You open the database with `rocksdb-haskell-jprupp` and create the event+publisher with `nqe`.++### With Stack++Recent Stackage nightly snapshots include Haskoin Node. With an older resolver,+add it and its dependencies to `stack.yaml`:++```yaml+extra-deps:+  - haskoin-node-2.3.1+  - haskoin-core-1.6.1+  - secp256k1-haskell-2.0.0+  - base16-2.0+  - rocksdb-haskell-jprupp-2.6.1+  - rocksdb-query-0.6.1+  - nqe-0.6.9+```++### Step by step with Stack++1. Install [Stack](https://docs.haskellstack.org/) and the libraries above.+2. Create a project:++   ```sh+   stack new node-demo simple+   cd node-demo+   ```++3. Add the packages listed under [With Cabal](#with-cabal) to `build-depends`+   in `node-demo.cabal`, plus `network`.+4. If your resolver is older than the packages, add the `extra-deps` shown+   under [With Stack](#with-stack) to `stack.yaml`.+5. Put the [quick start](#quick-start) program in `src/Main.hs` and run it:++   ```sh+   stack run+   ```++   Within a few seconds it logs connections and starts printing header+   heights. A full header sync of Bitcoin usually takes minutes and on the+   order of a hundred megabytes of disk; it resumes where it stopped if you+   interrupt it.++## Quick start++This program connects to Bitcoin, syncs headers, and prints what happens:++```haskell+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedRecordDot #-}++import Control.Monad (forever)+import Data.Default (def)+import Database.RocksDB (Config (..), withDB)+import Haskoin (NetworkAddress (..), btc, sockToHostAddress)+import Haskoin.Node+import NQE (receive, withPublisher, withSubscription)+import Network.Socket (SockAddr (..))++main :: IO ()+main =+  withLogger LevelInfo $ \logger ->+    withDB "/tmp/haskoin-node" def {createIfMissing = True} $ \db ->+      withPublisher $ \pub ->+        withSubscription pub $ \events -> do+          let cfg =+                NodeConfig+                  { maxPeers = 8,+                    db = db,+                    cf = Nothing,+                    peers = [],+                    discover = True,+                    address = NetworkAddress 0 (sockToHostAddress (SockAddrInet 0 0)),+                    net = btc,+                    pub = pub,+                    timeout = 120,+                    maxPeerLife = 48 * 3600,+                    connect = withConnection,+                    logger = logger+                  }+          withNode cfg $ \_node -> forever $+            receive events >>= \case+              ChainEvent (ChainBestBlock bn) ->+                putStrLn ("Best header: " <> show bn.height)+              ChainEvent (ChainSynced _) ->+                putStrLn "Headers in sync"+              PeerEvent (PeerConnected p) ->+                putStrLn ("Connected: " <> show p)+              _ -> return ()+```++Subscribe to the publisher before calling `withNode`, as here, or you may miss+the first events.++## Tutorial: a block watcher++This builds a program that prints every new Bitcoin block with its number of+transactions. It shows the intended division of labour: the node follows+headers and tells you about them, and you fetch what you need.++### 1. Wait until the headers are synced++At start-up the node downloads hundreds of thousands of headers, each one a+`ChainBestBlock` event. Reacting to each of those would mean fetching every+historical block. Wait for `ChainSynced` first:++```haskell+waitSynced :: Inbox NodeEvent -> IO ()+waitSynced events =+  receive events >>= \case+    ChainEvent (ChainSynced _) -> return ()+    _ -> waitSynced events+```++### 2. React to new best headers++After that, each `ChainBestBlock` is a new block at the tip, or the new tip+after a reorganisation:++```haskell+watch :: Network -> Node -> Inbox NodeEvent -> IO ()+watch net node events = forever $+  receive events >>= \case+    ChainEvent (ChainBestBlock bn) -> fetch net node bn+    _ -> return ()+```++### 3. Fetch the block from the fastest peer++`getPeers` lists online peers, fastest first. Ask one, and try the next if it+fails or times out:++```haskell+fetch :: Network -> Node -> BlockNode -> IO ()+fetch net node bn = getPeers node.peerMgr >>= go+  where+    hash = headerHash bn.header+    go [] = putStrLn ("No peer could send block " <> show bn.height)+    go (op : ops) =+      getBlocks net 30 op.mailbox [hash] >>= \case+        Just [b] ->+          putStrLn (show bn.height <> ": " <> show (length b.txs) <> " transactions")+        _ -> go ops+```++### 4. Put it together++Use the configuration from the [quick start](#quick-start), and run the watcher+inside `withNode`:++```haskell+withNode cfg $ \node -> do+  waitSynced events+  watch btc node events+```++### What the node did for you++While this runs, the node keeps eight connections, measures and ranks them,+drops silent or misbehaving peers and finds replacements, stores headers so a+restart is quick, and switches to a heavier chain if one appears. The watcher is+only twenty lines because none of that is its concern.++## Concepts++### Components++`withNode` starts two processes, built with [NQE](https://github.com/jprupp/nqe):++- The **peer manager** (`Haskoin.Node.PeerMgr`) finds peer addresses, connects+  to them, does the version handshake, pings them, and drops those that are slow,+  silent, misbehaving or too old.+- The **chain** (`Haskoin.Node.Chain`) asks one peer at a time for headers,+  connects them to the headers it has, follows the chain with the most work, and+  stores the headers in RocksDB.++Each peer connection is its own process (`Haskoin.Node.Peer`), run by a+supervisor inside the peer manager.++### Events++Everything is published to the `Publisher NodeEvent` in the configuration:++| Event                                   | When                                                    |+| --------------------------------------- | ------------------------------------------------------- |+| `ChainEvent (ChainBestBlock node)`      | The best header changed, including on start             |+| `ChainEvent (ChainSynced node)`         | Headers caught up with the network (once)               |+| `PeerEvent (PeerConnected peer)`        | A peer finished the handshake                           |+| `PeerEvent (PeerDisconnected peer)`     | An online peer went away                                |+| `PeerEvent (PeerMessage peer msg)`      | A peer sent a message: `MInv`, `MBlock`, `MTx` and so on |++"In sync" means the best header is less than two hours old, has at least the+minimum chain work of the network, and no peer is left to sync from.++### Back-pressure++A peer may have at most 16 messages published and not yet handled by the node.+Past that, the node stops reading its socket, so a fast peer fills its own TCP+window instead of the node's memory. Your subscriber should keep reading events;+a subscription that falls far behind holds memory.++### Database layout++Headers are stored in the column family you configure, or the default one:++| Key prefix | Value                         |+| ---------- | ----------------------------- |+| `0x90`     | header (`BlockNode`) by hash  |+| `0x91`     | best header                   |+| `0x92`     | data version                  |++You can keep your own data in the same database, as Haskoin Store does, as long+as your keys do not start with these bytes.++## Usage guide++### Picking a peer and requesting data++```haskell+ps <- getPeers node.peerMgr -- online peers, fastest first+case ps of+  op : _ -> do+    let p = op.mailbox+    mblocks <- getBlocks btc 30 p [blockHash] -- Maybe [Block]+    mtxs <- getTxs btc 30 p [txHash] -- Maybe [Tx]+    sendMessage MMempool p+  [] -> return ()+```++`getBlocks`, `getTxs` and `getData` return `Nothing` if the peer does not send+everything, in order, before the timeout in seconds. `pingPeer` measures a round+trip.++### Querying headers++```haskell+best <- chainGetBest node.chain+mnode <- chainGetBlock node.chain someHash+isMain <- chainBlockMain node.chain someHash+fork <- chainGetSplitBlock node.chain nodeA nodeB+parents <- chainGetParents node.chain height top+synced <- chainIsSynced node.chain+```++`runChainT` runs the header functions of haskoin-core, such as `getAncestor`,+against the database of the chain.++### Static peers++```haskell+cfg {peers = ["node1.example.com", "10.0.0.5:8333", "[2001:db8::1]:8333"], discover = False}+```++A peer without a port uses the default port of the network.++### Custom connections++`connect` decides how to open a connection. `withConnection` opens a plain TCP+socket. Replace it to go through a proxy, or to plug in mock peers in tests, as+the test suite does.++### Logging++`withLogger level` logs to standard error, one line per message, such as+`[Info#Chain] Best block header at height 870000`. Build a `Logger` with your+own `push` function to log elsewhere.++## Configuration++| Field         | Meaning                                                                   |+| ------------- | ------------------------------------------------------------------------- |+| `maxPeers`    | Maximum number of peers to connect to                                     |+| `db`          | RocksDB handle for the headers                                            |+| `cf`          | Column family for the headers, or `Nothing` for the default one           |+| `peers`       | Static peers, as `host`, `host:port` or `[ipv6]:port`                     |+| `discover`    | Use DNS seeds and addresses relayed by peers                              |+| `address`     | This node's address, sent in version messages                             |+| `net`         | Network constants from haskoin-core (`btc`, `bch`, `btcTest`, ...)        |+| `pub`         | Publisher that receives every `NodeEvent`                                 |+| `timeout`     | Seconds of silence, or without a pong, before a peer is dropped           |+| `maxPeerLife` | Seconds after which a peer is dropped, so that connections rotate         |+| `connect`     | How to open a connection; usually `withConnection`                        |+| `logger`      | Where to log; see `withLogger`                                            |++## Module overview++`Haskoin.Node` is the only exposed module. It re-exports:++| Part           | Contents                                                                    |+| -------------- | --------------------------------------------------------------------------- |+| Node           | `NodeConfig`, `NodeEvent`, `Node`, `withNode`, `withConnection`              |+| Peers          | `Peer`, `PeerEvent`, `sendMessage`, `getBlocks`, `getTxs`, `getData`, `pingPeer`, `killPeer` |+| Peer manager   | `PeerMgr`, `OnlinePeer`, `getPeers`, `getOnlinePeer`, `toSockAddr`, `isRoutable` |+| Chain          | `Chain`, `ChainEvent`, `chainGetBest`, `chainGetBlock`, `chainGetAncestor`, `chainGetParents`, `chainGetSplitBlock`, `chainBlockMain`, `chainIsSynced`, `runChainT` |+| Logging        | `Logger`, `LogLevel`, `withLogger`, `withSource`, `logDebug` ... `logError`  |++The lower-level functions that feed messages to the peer manager and the chain+are exported too, for programs that wire the components themselves.++## Security notes++- Every message header is checked before its payload is read. The payload limit+  is the `maxPayloadSize` of the command on the network: on Bitcoin Cash, 2 MiB+  for everything but block messages.+- List messages are refused as soon as their count is over the limit, such as+  2000 headers, 1000 addresses or 50000 inventory entries.+- A peer that sends anything invalid is disconnected.+- Addresses relayed by peers are only used if they are public unicast+  addresses, so a peer cannot make the node connect to services on its own host+  or network. Static peers are not checked.+- At most 4096 relayed addresses are kept.+- Headers are not reported as synced until they have the minimum chain work of+  the network. Above that work, forks from far below the tip on a chain with+  less work are refused.+- The node trusts the chain with the most work. It does not validate blocks:+  check the data you get from peers before relying on it.++## Development++```sh+git clone https://github.com/jprupp/haskoin-node.git+cd haskoin-node++stack build                        # build the library+stack test                         # run the tests+stack haddock --no-haddock-deps    # build the API documentation+```++On NixOS, or with `stack --nix`, the `nix` section of `stack.yaml` provides+RocksDB and libsecp256k1.++`package.yaml` is the source of the package description. Change it rather than+`haskoin-node.cabal`, which [hpack](https://github.com/sol/hpack) generates.++The tests are in `test/` and use [hspec](https://hspec.github.io/). They run+against mock peers on the Bitcoin Cash regression test network, so they need no+Internet access.++### Contributing++Pull requests are welcome.++- Format the code with [Ormolu](https://github.com/tweag/ormolu).+- Add tests for the behaviour you change.+- Describe the change under a new version in `CHANGELOG.md`. The file follows+  [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and versions follow+  [Semantic Versioning](https://semver.org/).++## Related projects++- [haskoin-core](https://github.com/jprupp/haskoin-core) has the Bitcoin data+  types and protocol that this library speaks.+- [haskoin-store](https://github.com/jprupp/haskoin-store) indexes blocks and+  transactions on top of this node and serves them over a REST API.+- [nqe](https://github.com/jprupp/nqe) provides the processes, supervisors and+  publishers.+- [rocksdb-query](https://github.com/jprupp/rocksdb-query) stores the headers.++## License++Haskoin Node is released under the+[MIT License](https://github.com/jprupp/haskoin-node/blob/master/LICENSE).
haskoin-node.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           haskoin-node-version:        2.3.0+version:        2.3.1 synopsis:       P2P library for Bitcoin and Bitcoin Cash description:    Please see the README on GitHub at <https://github.com/jprupp/haskoin-node#readme> category:       Bitcoin, Finance, Network
src/Haskoin/Node.hs view
@@ -8,16 +8,120 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NoFieldSelectors #-} +-- |+-- Module      : Haskoin.Node+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- A peer-to-peer node for Bitcoin (BTC) and Bitcoin Cash (BCH).+--+-- The node connects to other peers on the network, keeps a number of them+-- online, and synchronises the block chain headers, which it stores in a+-- RocksDB database. It does not download or validate full blocks or+-- transactions by itself: it publishes events and lets the application request+-- whatever data it needs from the connected peers.+--+-- A node is made of two main components, which run in their own threads:+--+-- * The peer manager ("Haskoin.Node.PeerMgr") discovers and connects to peers,+--   performs the version handshake, pings peers to measure latency, and+--   disconnects peers that misbehave, are too slow or have been connected for+--   too long.+--+-- * The chain ("Haskoin.Node.Chain") downloads block headers from peers,+--   connects them to the best chain, and stores them in the database.+--+-- Everything that happens is published as a 'NodeEvent' to the 'Publisher'+-- given in 'NodeConfig'. Subscribe to it to learn about new best blocks, peers+-- connecting and disconnecting, and every message received from a peer, such as+-- inventory announcements, blocks and transactions.+--+-- = Purpose+--+-- Talking to the Bitcoin network well is a job of its own, independent of what+-- a program does with the data. Peers have to be found, kept, measured and+-- dropped; messages have to be parsed defensively, because anyone can connect;+-- and the header chain, the backbone that tells which blocks are real, has to+-- be followed through reorganisations. This library does that job and nothing+-- else, so that wallets, indexers and monitors can be built on it without each+-- reimplementing it.+--+-- = Design+--+-- * __Headers only.__ The node syncs block headers, which are small and enough+--   to know the best chain, and leaves blocks and transactions to the+--   application. Applications differ wildly in what they need: an indexer wants+--   every block, a wallet a few transactions, a monitor nothing but+--   announcements. Fetching on request serves all of them.+--+-- * __Events, not callbacks.__ Everything the node learns is published. The+--   node does not know who listens or how many listeners there are, and a slow+--   listener cannot reach into the node's own state.+--+-- * __Processes with one job each.__ The peer manager, the chain and every+--   peer connection are separate NQE processes. Each owns its state and is fed+--   by messages, which keeps the concurrency easy to reason about and lets a+--   failing peer die without disturbing the rest.+--+-- * __Trust nothing from the network.__ Peers are strangers. Every message is+--   checked against size limits before it is read, invalid input disconnects+--   the peer, and a peer cannot make the node allocate memory faster than it+--   can work.+--+-- = Example+--+-- @+-- main :: IO ()+-- main =+--   'withLogger' 'LevelInfo' $ \\logger ->+--   withDB "\/tmp\/node" def{createIfMissing = True} $ \\db ->+--   'withPublisher' $ \\pub ->+--   'withSubscription' pub $ \\events -> do+--     let cfg =+--           'NodeConfig'+--             { maxPeers = 8,+--               db = db,+--               cf = Nothing,+--               peers = [],+--               discover = True,+--               address = NetworkAddress 0 (sockToHostAddress (SockAddrInet 0 0)),+--               net = btc,+--               pub = pub,+--               timeout = 120,+--               maxPeerLife = 48 * 3600,+--               connect = 'withConnection',+--               logger = logger+--             }+--     'withNode' cfg $ \\node -> forever $+--       'receive' events >>= \\case+--         'ChainEvent' ('ChainBestBlock' bn) -> print bn.height+--         _ -> return ()+-- @+--+-- = Requesting data+--+-- Use 'getPeers' to list the peers that are online, then 'getBlocks',+-- 'getTxs' or 'getData' to request data from one of them. Use+-- 'chainGetBlock', 'chainGetBest' and related functions to query the headers+-- that the node has stored. module Haskoin.Node-  ( module Haskoin.Node.Peer,-    module Haskoin.Node.PeerMgr,-    module Haskoin.Node.Chain,-    module Haskoin.Node.Logger,+  ( -- * Node     NodeConfig (..),     NodeEvent (..),     Node (..),     withNode,     withConnection,+    -- * Peers+    module Haskoin.Node.Peer,+    -- * Peer manager+    module Haskoin.Node.PeerMgr,+    -- * Chain+    module Haskoin.Node.Chain,+    -- * Logging+    module Haskoin.Node.Logger,   ) where @@ -38,7 +142,10 @@ import Text.Read (readMaybe) import UnliftIO --- | General node configuration.+-- | General node configuration. Passed to 'withNode'.+--+-- Connecting is a parameter ('connect') rather than hard-wired so that tests can+-- plug in simulated peers, and deployments can route through a proxy. data NodeConfig = NodeConfig   { -- | maximum number of connected peers allowed     maxPeers :: !Int,@@ -56,25 +163,40 @@     net :: !Network,     -- | node events are sent to this publisher     pub :: !(Publisher NodeEvent),-    -- | timeout in seconds+    -- | seconds without a response before a peer is considered unresponsive+    -- and disconnected; also used as timeout for handshakes and for syncing+    -- headers     timeout :: !NominalDiffTime,-    -- | peer disconnect after seconds+    -- | disconnect a peer after it has been connected for this many seconds,+    -- so that connections rotate over time     maxPeerLife :: !NominalDiffTime,+    -- | how to open a connection to a peer address; normally+    -- 'withConnection', but can be replaced to use a proxy or for testing     connect :: !(SockAddr -> WithConnection),     -- | log messages here     logger :: !Logger   } +-- | A running node, as given by 'withNode'. Use its fields to query and+-- control the peer manager and the chain. data Node = Node-  { peerMgr :: !PeerMgr,+  { -- | the peer manager+    peerMgr :: !PeerMgr,+    -- | the block header chain     chain :: !Chain   } +-- | Events published by the node to the 'Publisher' in 'NodeConfig'. data NodeEvent-  = ChainEvent !ChainEvent-  | PeerEvent !PeerEvent+  = -- | something happened to the header chain, such as a new best block+    ChainEvent !ChainEvent+  | -- | a peer connected, disconnected or sent a message+    PeerEvent !PeerEvent   deriving (Eq) +-- | Open a TCP connection to a peer address and pass its conduits to a+-- function. The connection is closed when the function returns. Calls 'error'+-- if the address cannot be converted into a host and port. withConnection :: SockAddr -> WithConnection withConnection na f =   fromSockAddr na >>= \case@@ -83,6 +205,8 @@       runTCPClient cset $ \ad ->         f (Conduits (appSource ad) (appSink ad)) +-- | Turn a socket address into client settings for a TCP connection, or+-- 'Nothing' if it cannot be converted. fromSockAddr :: (MonadUnliftIO m) => SockAddr -> m (Maybe ClientSettings) fromSockAddr sa = go `catch` e   where@@ -96,6 +220,9 @@     e :: (Monad m) => SomeException -> m (Maybe a)     e _ = return Nothing +-- | Forward events from the chain to the node publisher, telling the peer+-- manager about every new best block height so that it can tell peers apart+-- that are behind. chainEvents ::   (MonadIO m) => PeerMgr -> Inbox ChainEvent -> Publisher NodeEvent -> m () chainEvents mgr input output = loopForever $ do@@ -145,6 +272,10 @@         _ -> return ()  -- | Launch node process in the foreground.+--+-- Start the chain and the peer manager in background threads, pass a 'Node'+-- handle to the given action, and stop everything when the action returns. If+-- any component fails, the exception is re-thrown in the calling thread. withNode ::   (MonadUnliftIO m) => NodeConfig -> (Node -> m a) -> m a withNode NodeConfig {..} action = flip runContT return $ do
src/Haskoin/Node/Chain.hs view
@@ -15,6 +15,57 @@ {-# LANGUAGE NoFieldSelectors #-} {-# OPTIONS_GHC -fno-warn-orphans #-} +-- |+-- Module      : Haskoin.Node.Chain+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The block header chain.+--+-- The chain process downloads block headers from connected peers, one peer at a+-- time, connects them to the headers it already has, and stores them in RocksDB.+-- It follows the chain with the most work, and publishes a 'ChainBestBlock'+-- event whenever the best block changes. Once the best block is recent (less+-- than two hours old), has at least the minimum chain work of the network, and+-- no peer is left to sync from, it publishes a single 'ChainSynced' event.+--+-- After a peer has delivered all its headers, it is asked to announce new+-- blocks with headers messages, which are fed back to the chain process by+-- "Haskoin.Node".+--+-- = Why it works this way+--+-- * __One peer at a time.__ Headers form a single chain, so asking several+--   peers for the same range at once would only download it several times.+--   The chain syncs from one peer and moves to the next when it is done or+--   times out, which also means every connected peer eventually gets a chance+--   to show a better chain.+--+-- * __Most work wins.__ The best chain is the one with the most accumulated+--   proof of work, not the longest or the first seen, as in every Bitcoin+--   implementation.+--+-- * __"Synced" has to be earned.__ Recent headers alone do not prove much: a+--   peer can feed a cheap chain with fresh timestamps. The chain only counts as+--   synced once its work reaches the network's minimum chain work, the same+--   guard Bitcoin Core uses before leaving initial block download. Applications+--   use 'ChainSynced' to start work that only makes sense at the tip, so a false+--   signal would be costly.+--+-- * __Headers are stored, not kept in memory.__ Storing them in RocksDB lets a+--   restart resume where it stopped instead of downloading the whole chain+--   again.+--+-- = Database+--+-- Headers are stored as 'BlockNode' values in the configured column family, or+-- the default one, under keys starting with byte @0x90@. The best block is+-- stored under key @0x91@ and a data version under key @0x92@. If the data+-- version does not match, keys @0x90@ and @0x91@ are deleted when the chain+-- starts. Applications that share the database must not use these prefixes. module Haskoin.Node.Chain   ( ChainConfig (..),     ChainEvent (..),@@ -70,31 +121,43 @@     net :: !Network,     -- | send header chain events here     pub :: !(Publisher ChainEvent),-    -- | timeout in seconds+    -- | seconds to wait for headers from the syncing peer before it is+    -- disconnected     timeout :: !NominalDiffTime,     -- | log messages here     logger :: !Logger   } +-- | Messages handled by the chain process. data ChainMessage-  = ChainHeaders !Peer ![BlockHeader]-  | ChainPeerConnected !Peer-  | ChainPeerDisconnected !Peer-  | ChainPing-  | ChainSyncMsg !(Listen ())+  = -- | a peer sent headers+    ChainHeaders !Peer ![BlockHeader]+  | -- | a peer completed the handshake+    ChainPeerConnected !Peer+  | -- | a peer disconnected+    ChainPeerDisconnected !Peer+  | -- | periodic tick to check the syncing peer for timeouts+    ChainPing+  | -- | reply once every previous message has been handled+    ChainSyncMsg !(Listen ())  -- | Events originating from chain syncing process. data ChainEvent   = -- | chain has new best block     ChainBestBlock !BlockNode-  | -- | chain is in sync with the network+  | -- | chain is in sync with the network; published only once, with the best+    -- block at that time     ChainSynced !BlockNode   deriving (Eq, Show) --- | State and configuration.+-- | Handle to a running chain process. Obtain one with 'withChain', or from+-- the 'Haskoin.Node.Node' returned by 'Haskoin.Node.withNode'. data Chain = Chain-  { config :: !ChainConfig,+  { -- | configuration+    config :: !ChainConfig,+    -- | mailbox of the chain process     mailbox :: !(Mailbox ChainMessage),+    -- | syncing state     state :: !(TVar ChainState)   } @@ -115,9 +178,13 @@     return ChainDataVersionKey   put ChainDataVersionKey = putWord8 0x92 +-- | State of the sync against one peer. data ChainSync = ChainSync-  { peer :: !Peer,+  { -- | peer being synced against+    peer :: !Peer,+    -- | when it last sent headers, or when syncing started     timestamp :: !UTCTime,+    -- | last header it sent, from which to continue syncing     best :: !(Maybe BlockNode)   } @@ -155,11 +222,17 @@     return BestBlockKey   put BestBlockKey = putWord8 0x91 +-- | Monad transformer with access to a 'Chain'. It has a 'BlockHeaders'+-- instance, so the header functions of haskoin-core, such as 'getAncestor' and+-- 'splitPoint', can run in it against the database of the chain. type ChainT m = ReaderT Chain m +-- | Run a 'ChainT' action against a chain. runChainT :: ChainT m a -> Chain -> m a runChainT = runReaderT +-- | Read and write headers in the database of a chain configuration. Orphan+-- instance. instance (MonadIO m) => BlockHeaders (ReaderT ChainConfig m) where   addBlockHeader bn = ReaderT $ \ChainConfig {db, cf} -> liftIO $ do     case cf of@@ -185,6 +258,7 @@         Nothing -> insertOp (BlockHeaderKey (h bn)) bn         Just cf' -> insertOpCF cf' (BlockHeaderKey (h bn)) bn +-- | Read and write headers in the database of a chain. Orphan instance. instance (MonadIO m) => BlockHeaders (ChainT m) where   addBlockHeader bn = withReaderT (.config) (addBlockHeader bn)   getBlockHeader bh = withReaderT (.config) (getBlockHeader bh)@@ -192,6 +266,13 @@   setBestBlockHeader bn = withReaderT (.config) (setBestBlockHeader bn)   addBlockHeaders bns = withReaderT (.config) (addBlockHeaders bns) +-- | Start the chain process and pass its handle to the given action. The process+-- is stopped when the action returns, and its exceptions are re-thrown in the+-- calling thread.+--+-- The database is initialised first: purged if its data version is old, and+-- seeded with the genesis block of the network if it has no best block. The+-- current best block is published as a 'ChainBestBlock' event on start. withChain ::   (MonadUnliftIO m) => ChainConfig -> (Chain -> m a) -> m a withChain cfg action = do@@ -218,6 +299,7 @@         msg <- receive inbox         chainConfigMessage ch msg +-- | Log and publish a chain event. chainEvent :: (MonadIO m) => ChainConfig -> ChainEvent -> m () chainEvent cfg e = do   case e of@@ -231,6 +313,10 @@         ("Headers in sync at height " <> cs (show b.height))   publish e cfg.pub +-- | Import headers sent by a peer. Disconnect the peer if they do not connect.+-- Publish a new best block if it changed. If the peer has no more headers, move+-- on to the next peer and check whether the chain is now in sync; otherwise ask+-- it for more. processHeaders :: (MonadIO m) => Chain -> Peer -> [BlockHeader] -> m () processHeaders ch p hs = do   let len = length hs@@ -259,6 +345,7 @@           syncNotif ch         else syncPeer ch p +-- | Start syncing against the next peer in the queue, unless a peer is already being synced. syncNewPeer :: (MonadIO m) => Chain -> m () syncNewPeer ch =   getSyncingPeer ch.state >>= \case@@ -270,6 +357,7 @@           logDebug ch.config.logger ("Syncing against peer " <> p.label)           syncPeer ch p +-- | Publish a 'ChainSynced' event if the chain just became synced. syncNotif :: (MonadIO m) => Chain -> m () syncNotif ch =   notifySynced ch >>= \case@@ -278,6 +366,8 @@       runReaderT getBestBlockHeader ch.config         >>= chainEvent ch.config . ChainSynced +-- | Ask a peer for headers, continuing from the last header it sent, or from the+-- best block. Does nothing if another peer is being synced. syncPeer :: (MonadIO m) => Chain -> Peer -> m () syncPeer ch p = do   t <- liftIO getCurrentTime@@ -306,6 +396,7 @@         Just h -> return h       Just <$> syncHeaders ch t h p +-- | Handle a message sent to the chain process. chainConfigMessage :: (MonadIO m) => Chain -> ChainMessage -> m () chainConfigMessage ch (ChainHeaders p hs) =   processHeaders ch p hs@@ -333,6 +424,8 @@       | otherwise -> return ()     Nothing -> syncNewPeer ch +-- | Run a loop in the background that sends a 'ChainPing' to the chain process+-- every 2 to 20 seconds. withSyncLoop :: (MonadUnliftIO m) => Mailbox ChainMessage -> m a -> m a withSyncLoop mbox mf =   withAsync go $ \a ->@@ -502,6 +595,8 @@     ChainState {syncing = Just ChainSync {peer}} -> return (Just peer)     _ -> return Nothing +-- | Reserve a peer with 'setBusy' and make it the syncing peer. Returns 'False'+-- if the peer is busy with something else. setSyncingPeer :: (MonadIO m) => Chain -> Peer -> m Bool setSyncingPeer ch p =   setBusy p >>= \case@@ -565,7 +660,8 @@ chainSyncingPeer :: (MonadIO m) => TVar ChainState -> m (Maybe ChainSync) chainSyncingPeer st = (.syncing) <$> readTVarIO st --- | Get a block header from the block chain.+-- | Get a block header from the block chain. Returns headers on side chains+-- too; use 'chainBlockMain' to check whether a block is in the main chain. chainGetBlock :: (MonadIO m) => Chain -> BlockHash -> m (Maybe BlockNode) chainGetBlock ch bh = runReaderT (getBlockHeader bh) ch.config @@ -573,12 +669,15 @@ chainGetBest :: (MonadIO m) => Chain -> m BlockNode chainGetBest ch = runReaderT getBestBlockHeader ch.config --- | Get ancestor of 'BlockNode' at 'BlockHeight' from chain process.+-- | Get ancestor of 'BlockNode' at 'BlockHeight' from chain process. Returns+-- 'Nothing' if the height is above that of the node. chainGetAncestor ::   (MonadIO m) => Chain -> BlockHeight -> BlockNode -> m (Maybe BlockNode) chainGetAncestor ch h bn = runReaderT (getAncestor h bn) ch.config  -- | Get parents of 'BlockNode' starting at 'BlockHeight' from chain process.+-- The result is in ascending order, from the block at the given height up to+-- the parent of the given node, which is not included. chainGetParents ::   (MonadIO m) => Chain -> BlockHeight -> BlockNode -> m [BlockNode] chainGetParents ch height top =@@ -592,12 +691,14 @@             Nothing -> return acc             Just p -> go (p : acc) p --- | Get last common block from chain process.+-- | Get last common block from chain process: the most recent block that is an+-- ancestor of both nodes. chainGetSplitBlock ::   (MonadIO m) => Chain -> BlockNode -> BlockNode -> m BlockNode chainGetSplitBlock ch l r = runReaderT (splitPoint l r) ch.config --- | Notify chain that a new peer is connected.+-- | Notify chain that a new peer is connected. It is added to the queue of+-- peers to sync against. chainPeerConnected :: (MonadIO m) => Chain -> Peer -> m () chainPeerConnected ch p =   ChainPeerConnected p `send` ch.mailbox@@ -617,12 +718,14 @@       bm@(Just bn) ->         (== bm) <$> chainGetAncestor ch bn.height bb --- | Is chain in sync with network?+-- | Is chain in sync with network? Once 'True', it stays 'True' even if the+-- chain falls behind later. chainIsSynced :: (MonadIO m) => Chain -> m Bool chainIsSynced ch =   (.beenInSync) <$> readTVarIO (ch.state) --- | Peer sends a bunch of headers to the chain process.+-- | Peer sends a bunch of headers to the chain process. "Haskoin.Node" calls+-- this for every headers message received from an online peer. chainHeaders :: (MonadIO m) => Chain -> Peer -> [BlockHeader] -> m () chainHeaders ch p hs =   ChainHeaders p hs `send` ch.mailbox
src/Haskoin/Node/Logger.hs view
@@ -6,6 +6,11 @@  -- | Logging to standard error. A 'Logger' is a plain value carried in -- configuration records, so code that logs only needs 'MonadIO'.+--+-- The node used to log through a logging monad transformer. That forced every+-- function into that monad, and made polymorphic loops leak memory when GHC did+-- not specialise them (see "Haskoin.Node.Loop"). A plain value avoids both, and+-- programs that log elsewhere can supply their own @push@ function. module Haskoin.Node.Logger   ( Logger (..),     LogLevel (..),@@ -31,9 +36,21 @@   ) import UnliftIO (MonadIO, bracket, liftIO) -data LogLevel = LevelDebug | LevelInfo | LevelWarn | LevelError+-- | Severity of a log message, from least to most severe.+data LogLevel+  = -- | detailed tracing, useful when debugging+    LevelDebug+  | -- | normal operation, such as peers connecting and blocks arriving+    LevelInfo+  | -- | unexpected events that the node recovers from+    LevelWarn+  | -- | failures+    LevelError   deriving (Eq, Ord, Show) +-- | Where and how to log. Every component of the node takes one in its+-- configuration. Build one with 'withLogger', or directly with any @push@+-- function to send log lines elsewhere. data Logger = Logger   { -- | messages below this level are dropped     level :: !LogLevel,@@ -50,11 +67,13 @@   bracket (newFastLogger1 (LogStderr defaultBufSize)) snd $ \(push, _) ->     go Logger {level, source = "", push} --- | Same output with a different tag.+-- | Same output with a different tag. The node uses this to tag the messages of+-- each component, such as @Chain@ and @PeerMgr@. withSource :: Text -> Logger -> Logger withSource source logger = logger {source} --- | Lines look like monad-logger's: @[Info#Chain] message@.+-- | Log a message at a level, if the level is at least that of the 'Logger'.+-- Lines look like monad-logger's: @[Info#Chain] message@. logAt :: (MonadIO m) => LogLevel -> Logger -> Text -> m () logAt l logger msg =   when (l >= logger.level) . liftIO . logger.push $@@ -64,6 +83,7 @@       | T.null logger.source = ""       | otherwise = "#" <> toLogStr logger.source +-- | Log at 'LevelDebug', 'LevelInfo', 'LevelWarn' or 'LevelError'. logDebug, logInfo, logWarn, logError :: (MonadIO m) => Logger -> Text -> m () logDebug = logAt LevelDebug logInfo = logAt LevelInfo
src/Haskoin/Node/Loop.hs view
@@ -12,6 +12,8 @@ -- stacks, so it never accumulates. module Haskoin.Node.Loop (loopForever) where +-- | Run an action over and over, forever, discarding its results. Only+-- exceptions stop the loop. loopForever :: (Monad m) => m a -> m b loopForever action = go   where
src/Haskoin/Node/Peer.hs view
@@ -11,6 +11,37 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE NoFieldSelectors #-} +-- |+-- Module      : Haskoin.Node.Peer+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- A single connection to a peer on the Bitcoin network.+--+-- Each peer runs as its own process. It reads messages from the network,+-- parses them, and publishes them as 'PeerEvent's. It also accepts messages to+-- send to the peer through its mailbox. Peers are normally started and managed+-- by "Haskoin.Node.PeerMgr"; applications mostly use 'sendMessage',+-- 'getBlocks', 'getTxs' and 'getData' on peers obtained from+-- 'Haskoin.Node.PeerMgr.getPeers'.+--+-- = Defensive by design+--+-- A peer is anyone on the Internet. The code here assumes the worst of it:+--+-- * The 24-byte header of each message is read and checked first: network+--   magic, and payload size against the limit for that command. Only then is+--   the payload read, so a peer cannot make the node buffer a huge message it+--   would refuse anyway.+--+-- * Anything that does not parse closes the connection. There is no attempt to+--   recover, because a peer that sends garbage once is not worth the risk.+--+-- * Reading pauses when the node falls behind ('maxPendingMessages'), so the+--   speed of the node, not the speed of the peer, sets the pace. module Haskoin.Node.Peer   ( PeerConfig (..),     PeerEvent (..),@@ -54,32 +85,55 @@ import System.Random import UnliftIO +-- | The two directions of a network connection, as conduits.+--+-- Connections are abstract conduits rather than sockets, so that the same peer+-- code runs over TCP, through a proxy, or against an in-memory mock in tests. data Conduits = Conduits-  { inboundConduit :: ConduitT () ByteString IO (),+  { -- | bytes received from the peer+    inboundConduit :: ConduitT () ByteString IO (),+    -- | bytes to send to the peer     outboundConduit :: ConduitT ByteString Void IO ()   } +-- | A function that opens a connection, passes its 'Conduits' to the given+-- action, and closes the connection when the action returns. type WithConnection = (Conduits -> IO ()) -> IO () +-- | Configuration of a peer process. data PeerConfig = PeerConfig-  { pub :: !(Publisher PeerEvent),+  { -- | peer events are published here+    pub :: !(Publisher PeerEvent),+    -- | network constants     net :: !Network,+    -- | name of the peer in logs, usually its address     label :: !Text,+    -- | how to open the connection to the peer     connect :: !WithConnection,+    -- | log messages here     logger :: !Logger   } +-- | Events published about peers. data PeerEvent-  = PeerConnected !Peer-  | PeerDisconnected !Peer-  | PeerMessage !Peer !Message+  = -- | the peer completed the version handshake and is online+    PeerConnected !Peer+  | -- | the peer disconnected+    PeerDisconnected !Peer+  | -- | the peer sent a message+    PeerMessage !Peer !Message   deriving (Eq) --- | Mailbox for a peer.+-- | Mailbox for a peer. Peers are compared by their mailbox, and shown by their+-- label. data Peer = Peer-  { mailbox :: !(Mailbox PeerMessage),+  { -- | send 'PeerMessage's to the peer process here+    mailbox :: !(Mailbox PeerMessage),+    -- | events from this peer are published here     pub :: !(Publisher PeerEvent),+    -- | name of the peer in logs, usually its address     label :: !Text,+    -- | whether the peer is reserved for a task; see 'setBusy'     busy :: !(TVar Bool),     -- | messages from this peer that the node has not finished handling     pending :: !(TVar Int)@@ -93,9 +147,13 @@  -- | Incoming messages that a peer accepts. data PeerMessage-  = KillPeer-  | SendMessage !Message+  = -- | disconnect from the peer+    KillPeer+  | -- | send a network message to the peer+    SendMessage !Message +-- | Build a 'Peer' handle from its configuration, its busy flag, its counter of+-- pending messages, and the mailbox of its process. wrapPeer ::   PeerConfig ->   TVar Bool ->@@ -125,6 +183,11 @@  -- | Run peer process in current thread. The 'Peer' must be the one that -- 'wrapPeer' built with the mailbox of this inbox.+--+-- Opens the connection, then reads and publishes messages from the peer while+-- sending the messages that arrive in the inbox. Returns when 'killPeer' is+-- called or the connection is closed, and throws if the peer sends invalid+-- data. peer ::   (MonadUnliftIO m) =>   PeerConfig ->@@ -231,17 +294,25 @@ outPeerConduit :: (Monad m) => Network -> ConduitT Message ByteString m () outPeerConduit net = awaitForever $ yield . runPut . putMessage net --- | Kill a peer with the provided exception.+-- | Ask a peer to disconnect. The peer process stops after sending any messages+-- queued before this one. killPeer :: (MonadIO m) => Peer -> m () killPeer p = KillPeer `send` p.mailbox --- | Send a network message to peer.+-- | Send a network message to peer. Returns as soon as the message is queued. sendMessage :: (MonadIO m) => Message -> Peer -> m () sendMessage msg p = SendMessage msg `send` p.mailbox +-- | Check whether a peer is reserved for a task. getBusy :: (MonadIO m) => Peer -> m Bool getBusy p = readTVarIO p.busy +-- | Try to reserve a peer for a task, such as syncing headers. Returns 'True' if+-- the peer was free and is now reserved, or 'False' if it was already busy.+--+-- Reservation exists because some conversations with a peer span many+-- messages, like downloading a long run of headers. Two tasks using the same peer+-- for such a conversation at once would confuse each other's replies. setBusy :: (MonadIO m) => Peer -> m Bool setBusy p =   atomically $ do@@ -249,12 +320,16 @@     unless b $ writeTVar p.busy True     return $ not b +-- | Release a peer reserved with 'setBusy'. setFree :: (MonadIO m) => Peer -> m () setFree p = atomically $ writeTVar p.busy False  -- | Request full blocks from peer. Will return 'Nothing' if the list of blocks -- returned by the peer is incomplete, comes out of order, or a timeout is -- reached.+--+-- The 'Int' argument is the timeout in seconds. On networks with SegWit,+-- blocks are requested with their witness data. getBlocks ::   (MonadUnliftIO m) =>   Network ->@@ -275,6 +350,9 @@ -- | Request transactions from peer. Will return 'Nothing' if the list of -- transactions returned by the peer is incomplete, comes out of order, or a -- timeout is reached.+--+-- The 'Int' argument is the timeout in seconds. Peers normally only serve+-- transactions that are in their mempool. getTxs ::   (MonadUnliftIO m) =>   Network ->@@ -295,6 +373,11 @@ -- | Request transactions and/or blocks from peer. Return 'Nothing' if any -- single inventory fails to be retrieved, if they come out of order, or if -- timeout is reached.+--+-- The first argument is the timeout in seconds. A ping is sent right after the+-- request; since peers answer in order, receiving the matching pong before all+-- items means that the peer will not send the rest. Transactions are returned+-- as 'Left' and blocks as 'Right', in the order they were requested. getData ::   (MonadUnliftIO m) => Int -> Peer -> GetData -> m (Maybe [Either Tx Block]) getData seconds p gd@(GetData ivs) =@@ -336,7 +419,7 @@     is_block _ = False  -- | Ping a peer and await response. Return 'False' if response not received--- before timeout.+-- before timeout. The first argument is the timeout in seconds. pingPeer :: (MonadUnliftIO m) => Int -> Peer -> m Bool pingPeer time p =   fmap isJust . withSubscription p.pub $ \sub -> do@@ -347,6 +430,8 @@         | p == p' && r == r' -> Just ()       _ -> Nothing +-- | Receive the next message from the given peer, discarding events about other+-- peers. filterReceive :: (MonadIO m) => Peer -> Inbox PeerEvent -> m Message filterReceive p inb =   receive inb >>= \case
src/Haskoin/Node/PeerMgr.hs view
@@ -11,6 +11,53 @@ {-# LANGUAGE TupleSections #-} {-# LANGUAGE NoFieldSelectors #-} +-- |+-- Module      : Haskoin.Node.PeerMgr+-- Copyright   : (c) Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch+-- Stability   : experimental+-- Portability : POSIX+--+-- The peer manager keeps the node connected to the network.+--+-- It maintains a pool of known peer addresses, filled from the static peers in+-- the configuration, from the DNS seeds of the network when discovery is on, and+-- from addresses relayed by connected peers. A background loop connects to new+-- peers while there are fewer than 'maxPeers' of them.+--+-- For each connection, the peer manager sends a version message, waits for the+-- peer's version and acknowledgement, and then announces the peer as connected+-- with a 'PeerConnected' event. Peers are pinged periodically to measure their+-- latency, and the list returned by 'getPeers' is sorted by median ping time, so+-- that the fastest peers come first. A peer is disconnected if it stops+-- responding, does not answer a ping in time, or has been connected for longer+-- than 'maxPeerLife'. A 'PeerDisconnected' event is published when an online+-- peer goes away.+--+-- Most of the @peerMgr*@ functions are used by "Haskoin.Node" to feed messages+-- received from peers to the peer manager; applications rarely need to call+-- them directly.+--+-- = Why it works this way+--+-- * __Peers are ranked by measured latency.__ The fastest peers deliver+--   blocks and transactions soonest, so applications that pick the first peer+--   from 'getPeers' get the best service without measuring anything themselves.+--+-- * __Connections rotate.__ Peers are dropped after 'maxPeerLife' even when+--   they behave. A node that keeps the same few peers forever is easier to+--   isolate from the honest network, and stuck with them if they degrade.+--   Rotation keeps the peer set fresh.+--+-- * __Silence is failure.__ A peer that sends nothing, or ignores a ping, for+--   longer than 'timeout' is dropped. Waiting for a peer that has gone away+--   would stall whatever was waiting on it.+--+-- * __Relayed addresses are untrusted.__ Peers gossip addresses, and a+--   malicious peer could gossip private or loopback addresses to make the node+--   probe its own network. Only public addresses are accepted, and only a+--   bounded number are kept, so gossip cannot exhaust memory. module Haskoin.Node.PeerMgr   ( PeerMgrConfig (..),     PeerEvent (..),@@ -58,52 +105,102 @@ import UnliftIO import UnliftIO.Concurrent +-- | Configuration of the peer manager. "Haskoin.Node" builds it from the+-- 'Haskoin.Node.NodeConfig'. data PeerMgrConfig = PeerMgrConfig-  { maxPeers :: !Int,+  { -- | maximum number of peers to connect to+    maxPeers :: !Int,+    -- | static peers to connect to, as @host@ or @host:port@; IPv6 addresses+    -- go in square brackets, as in @[::1]:8333@     peers :: ![String],+    -- | connect to the network seeds and to addresses relayed by peers     discover :: !Bool,+    -- | address of this node, sent in version messages     address :: !NetworkAddress,+    -- | network constants     net :: !Network,+    -- | peer events are published here     pub :: !(Publisher PeerEvent),+    -- | seconds without a message from a peer, or without an answer to a+    -- ping, before it is disconnected     timeout :: !NominalDiffTime,+    -- | seconds after which a peer is disconnected regardless     maxPeerLife :: !NominalDiffTime,+    -- | how to open a connection to a peer address     connect :: !(SockAddr -> WithConnection),+    -- | log messages here     logger :: !Logger   } +-- | Handle to a running peer manager. Obtain one with 'withPeerMgr', or from+-- the 'Haskoin.Node.Node' returned by 'Haskoin.Node.withNode'. data PeerMgr = PeerMgr-  { config :: !PeerMgrConfig,+  { -- | configuration+    config :: !PeerMgrConfig,+    -- | supervisor that runs the peer processes     supervisor :: !Supervisor,+    -- | mailbox of the peer manager process     mailbox :: !(Mailbox PeerMgrMessage),+    -- | height of the best block, sent to peers in version messages     best :: !(TVar BlockHeight),+    -- | known addresses not yet connected to, at most 'maxKnownAddresses'     addresses :: !(TVar (Set SockAddr)),+    -- | peers being connected to or online, fastest first     peers :: !(TVar [OnlinePeer])   } +-- | Messages handled by the peer manager process. data PeerMgrMessage-  = Connect !SockAddr-  | CheckPeer !Peer-  | PeerDied !Child-  | ManagerBest !BlockHeight-  | PeerVerAck !Peer-  | PeerVersion !Peer !Version-  | PeerPing !Peer !Word64-  | PeerPong !Peer !Word64-  | PeerAddrs !Peer ![NetworkAddress]-  | Sync !(Listen ())+  = -- | connect to a peer address+    Connect !SockAddr+  | -- | check whether a peer timed out or must be pinged+    CheckPeer !Peer+  | -- | a peer process stopped+    PeerDied !Child+  | -- | the height of the best block changed+    ManagerBest !BlockHeight+  | -- | a peer acknowledged our version+    PeerVerAck !Peer+  | -- | a peer sent its version+    PeerVersion !Peer !Version+  | -- | a peer pinged us with a nonce+    PeerPing !Peer !Word64+  | -- | a peer answered a ping with a nonce+    PeerPong !Peer !Word64+  | -- | a peer relayed addresses+    PeerAddrs !Peer ![NetworkAddress]+  | -- | reply once every previous message has been handled+    Sync !(Listen ())  -- | Data structure representing an online peer.+--+-- Peers are compared for equality by their 'Peer' handle, and ordered by the+-- median of their recent ping times, so that sorting a list puts the fastest+-- peers first. Peers without any ping measurement sort as if their median ping+-- were 60 seconds. data OnlinePeer = OnlinePeer-  { address :: !SockAddr,+  { -- | network address of the peer+    address :: !SockAddr,+    -- | whether the peer acknowledged our version     verack :: !Bool,+    -- | whether the handshake is complete: both version and verack received     online :: !Bool,+    -- | version message sent by the peer, if received     version :: !(Maybe Version),+    -- | thread running the peer process     async :: !(Async ()),+    -- | handle to talk to the peer     mailbox :: !Peer,+    -- | nonce of the version message we sent, used to detect connections to+    -- ourselves     nonce :: !Word64,+    -- | time and nonce of the ping awaiting an answer, if any     ping :: !(Maybe (UTCTime, Word64)),+    -- | round-trip times of the last eleven pings, sorted     pings :: ![NominalDiffTime],+    -- | when the connection was made     connected :: !UTCTime,+    -- | when the peer last sent a message     tickled :: !UTCTime   } @@ -117,6 +214,12 @@     where       f OnlinePeer {pings = pings} = fromMaybe 60 (median pings) +-- | Start a peer manager and pass it to the given action. The peer manager and+-- all its peers are stopped when the action returns.+--+-- The peer manager does not connect to any peer until it learns the height of+-- the best block through 'peerMgrBest'. "Haskoin.Node" does this as soon as the+-- chain is loaded. withPeerMgr ::   (MonadUnliftIO m) =>   PeerMgrConfig ->@@ -143,6 +246,8 @@     go mgr ibx = withAsync (peerManager mgr ibx) $ \a ->       withConnectLoop mgr (link a >> action mgr) +-- | Main loop of the peer manager process. Waits for the best block height, then+-- handles messages forever. peerManager ::   (MonadUnliftIO m) => PeerMgr -> Inbox PeerMgrMessage -> m () peerManager mgr ibx = do@@ -154,25 +259,31 @@     logDebug mgr.config.logger "Awaiting event..."     dispatch mgr =<< receive ibx +-- | Store the height of the best block. putBestBlock :: (MonadIO m) => PeerMgr -> BlockHeight -> m () putBestBlock mgr bb = atomically $ writeTVar mgr.best bb +-- | Read the height of the best block. getBestBlock :: (MonadIO m) => PeerMgr -> m BlockHeight getBestBlock mgr = readTVarIO mgr.best +-- | Network constants of the peer manager. getNetwork :: PeerMgr -> Network getNetwork mgr = mgr.config.net +-- | Add the static peers and, if discovery is on, the network seeds to the known addresses. loadPeers :: (MonadIO m) => PeerMgr -> m () loadPeers mgr = do   loadStaticPeers mgr   loadNetSeeds mgr +-- | Resolve the static peers in the configuration and add them to the known addresses. loadStaticPeers :: (MonadIO m) => PeerMgr -> m () loadStaticPeers mgr =   mapM_ (newPeer mgr) . concat     =<< mapM (liftIO . toSockAddr mgr.config.net) mgr.config.peers +-- | If discovery is on, resolve the DNS seeds of the network and add them to the known addresses. loadNetSeeds :: (MonadIO m) => PeerMgr -> m () loadNetSeeds mgr =   when mgr.config.discover $ do@@ -181,6 +292,7 @@         <$> mapM (liftIO . toSockAddr mgr.config.net) mgr.config.net.seeds     mapM_ (newPeer mgr) ss +-- | Log how many peers are online out of the maximum. logConnectedPeers :: (MonadIO m) => PeerMgr -> m () logConnectedPeers mgr = do   let m = mgr.config.maxPeers@@ -189,15 +301,19 @@     mgr.config.logger     ("Peers connected: " <> cs (show l) <> "/" <> cs (show m)) +-- | All peers being connected to or online, fastest first. getOnlinePeers :: (MonadIO m) => PeerMgr -> m [OnlinePeer] getOnlinePeers mgr = readTVarIO mgr.peers +-- | Peers that completed the handshake, fastest first. getConnectedPeers :: (MonadIO m) => PeerMgr -> m [OnlinePeer] getConnectedPeers mgr = filter (.online) <$> getOnlinePeers mgr +-- | Publish a peer event. managerEvent :: (MonadIO m) => PeerMgr -> PeerEvent -> m () managerEvent mgr e = publish e mgr.config.pub +-- | Handle a message sent to the peer manager. dispatch ::   (MonadUnliftIO m) => PeerMgr -> PeerMgrMessage -> m () dispatch mgr (PeerVersion p v) = do@@ -273,12 +389,17 @@   logDebug mgr.config.logger ("Housekeeping for peer " <> p.label)   checkPeer mgr p +-- | Record that a peer just sent a message, so that it is not disconnected for+-- being idle. "Haskoin.Node" calls this for every message received. ticklePeer :: (MonadIO m) => PeerMgr -> Peer -> m () ticklePeer mgr p = do   logDebug mgr.config.logger ("Tickling peer " <> p.label)   t <- liftIO getCurrentTime   atomically (modifyPeer mgr.peers p (\o -> o {tickled = t})) +-- | Disconnect a peer that has been connected for too long, has been idle for+-- too long, or did not answer a ping in time. Otherwise ping it if no ping is+-- pending. checkPeer :: (MonadIO m) => PeerMgr -> Peer -> m () checkPeer mgr p =   atomically (mgr.peers `findPeer` p) >>= \case@@ -303,6 +424,7 @@     Nothing -> do       logError mgr.config.logger ("Peer " <> p.label <> " is offline") +-- | Send a ping with a random nonce to an online peer and remember when it was sent. sendPing :: (MonadIO m) => PeerMgr -> Peer -> m () sendPing mgr p = do   atomically (mgr.peers `findPeer` p) >>= \case@@ -322,6 +444,7 @@             mgr.config.logger             ("Will not ping offline peer " <> p.label) +-- | Clean up after a peer process stops, and publish a disconnection event if the peer was online. processPeerOffline :: (MonadIO m) => PeerMgr -> Child -> m () processPeerOffline mgr a = do   atomically (findPeerAsync mgr.peers a) >>= \case@@ -340,6 +463,7 @@       atomically (removePeer mgr.peers o.mailbox)       logConnectedPeers mgr +-- | Publish a connection event for a peer that just completed the handshake. announcePeer :: (MonadIO m) => PeerMgr -> Peer -> m () announcePeer mgr p = do   atomically (findPeer mgr.peers p) >>= \case@@ -352,6 +476,7 @@     Nothing ->       logWarn mgr.config.logger ("Not announcing disconnected peer " <> p.label) +-- | Take an address from the known addresses. If there are none, reload the static peers and seeds first. getNewPeer :: (MonadIO m) => PeerMgr -> m (Maybe SockAddr) getNewPeer mgr =   g >>= \case@@ -364,6 +489,8 @@           mgr.addresses           (first (listToMaybe . Set.elems) . Set.splitAt 1) +-- | Start a peer process for an address and send it our version, unless there+-- are no free slots or the address is already connected. connectPeer :: (MonadUnliftIO m) => PeerMgr -> SockAddr -> m () connectPeer mgr sa = do   n <- length <$> getOnlinePeers mgr@@ -424,6 +551,8 @@     launch pc inbox p =       withPeerLoop mgr p (\a -> link a >> peer pc p inbox) +-- | Run a loop in the background that asks the peer manager to check a peer at+-- random intervals between a quarter and a half of the timeout. withPeerLoop :: (MonadUnliftIO m) => PeerMgr -> Peer -> (Async a -> m a) -> m a withPeerLoop mgr p =   withAsync . loopForever $ do@@ -433,6 +562,8 @@     threadDelay r     managerCheck mgr p +-- | Run a loop in the background that connects to a new peer every 0.1 to 5+-- seconds while there are free slots. withConnectLoop :: (MonadUnliftIO m) => PeerMgr -> m a -> m a withConnectLoop mgr act =   withAsync go $ \a -> link a >> act@@ -450,6 +581,7 @@ maxKnownAddresses :: Int maxKnownAddresses = 4096 +-- | Add an address to the known addresses, unless it is connected already or the pool is full. newPeer :: (MonadIO m) => PeerMgr -> SockAddr -> m () newPeer mgr sa =   atomically $@@ -460,6 +592,7 @@           then known           else Set.insert sa known +-- | Record the round-trip time of a ping if the nonce matches the pending one. gotPong :: TVar [OnlinePeer] -> Word64 -> UTCTime -> Peer -> STM () gotPong b nonce now p =   findPeer b p >>= \case@@ -469,10 +602,17 @@       insertPeer b o'     _ -> return () +-- | Remember the time and nonce of a ping sent to a peer. setPeerPing :: TVar [OnlinePeer] -> Word64 -> UTCTime -> Peer -> STM () setPeerPing b nonce now p =   modifyPeer b p $ \o -> o {ping = Just (now, nonce)} +-- | Store the version of a peer. Returns 'Nothing' to reject it if the peer does+-- not serve the full chain, is ourselves, is unknown, or already sent a version.+--+-- A peer must offer the full chain ('nodeNetwork') to be useful for syncing, and+-- a version whose nonce matches one of ours means the node has connected to+-- itself, which happens when it learns its own address from gossip. setPeerVersion :: TVar [OnlinePeer] -> Peer -> Version -> STM (Maybe OnlinePeer) setPeerVersion b p v   | v.services .&. nodeNetwork == 0 = return Nothing@@ -490,6 +630,8 @@                 insertPeer b n                 return (Just n) +-- | Record that a peer acknowledged our version. Returns 'Nothing' if the peer is+-- unknown or already sent one. setPeerVerAck :: TVar [OnlinePeer] -> Peer -> STM (Maybe OnlinePeer) setPeerVerAck b p =   findPeer b p >>= \case@@ -500,57 +642,76 @@       return (Just o')     Nothing -> return Nothing +-- | Find a peer in the list. findPeer :: TVar [OnlinePeer] -> Peer -> STM (Maybe OnlinePeer) findPeer b p =   find ((== p) . (.mailbox))     <$> readTVar b +-- | Add or replace a peer in the list, keeping it sorted. insertPeer :: TVar [OnlinePeer] -> OnlinePeer -> STM () insertPeer b o =   modifyTVar b $ \x -> sort . nub $ o : x +-- | Change a peer in the list, if it is there. modifyPeer :: TVar [OnlinePeer] -> Peer -> (OnlinePeer -> OnlinePeer) -> STM () modifyPeer b p f =   findPeer b p >>= \case     Nothing -> return ()     Just o -> insertPeer b $ f o +-- | Remove a peer from the list. removePeer :: TVar [OnlinePeer] -> Peer -> STM () removePeer b p = modifyTVar b (filter ((/= p) . (.mailbox))) +-- | Find a peer by the thread that runs it. findPeerAsync :: TVar [OnlinePeer] -> Async () -> STM (Maybe OnlinePeer) findPeerAsync b a = find ((== a) . (.async)) <$> readTVar b +-- | Find a peer by its address. findPeerAddress :: TVar [OnlinePeer] -> SockAddr -> STM (Maybe OnlinePeer) findPeerAddress b a = find ((== a) . (.address)) <$> readTVar b +-- | Peers that completed the handshake, sorted so that those with the lowest+-- median ping come first. getPeers :: (MonadIO m) => PeerMgr -> m [OnlinePeer] getPeers = getConnectedPeers +-- | Information about a peer, or 'Nothing' if the peer manager does not know it.+-- The peer may still be in the middle of the handshake; check 'online'. getOnlinePeer :: (MonadIO m) => PeerMgr -> Peer -> m (Maybe OnlinePeer) getOnlinePeer mgr p = atomically (mgr.peers `findPeer` p) +-- | Ask the peer manager to check a peer. managerCheck :: (MonadIO m) => PeerMgr -> Peer -> m () managerCheck mgr p = CheckPeer p `send` mgr.mailbox +-- | Ask the peer manager to connect to an address. managerConnect :: (MonadIO m) => PeerMgr -> SockAddr -> m () managerConnect mgr sa = Connect sa `send` mgr.mailbox +-- | Tell the peer manager the height of the best block. peerMgrBest :: (MonadIO m) => PeerMgr -> BlockHeight -> m () peerMgrBest mgr bh = ManagerBest bh `send` mgr.mailbox +-- | Tell the peer manager that a peer acknowledged our version. peerMgrVerAck :: (MonadIO m) => PeerMgr -> Peer -> m () peerMgrVerAck mgr p = PeerVerAck p `send` mgr.mailbox +-- | Tell the peer manager that a peer sent its version. peerMgrVersion :: (MonadIO m) => PeerMgr -> Peer -> Version -> m () peerMgrVersion mgr p ver = PeerVersion p ver `send` mgr.mailbox +-- | Tell the peer manager that a peer sent a ping, so that it answers with a pong. peerMgrPing :: (MonadIO m) => PeerMgr -> Peer -> Word64 -> m () peerMgrPing mgr p nonce = PeerPing p nonce `send` mgr.mailbox +-- | Tell the peer manager that a peer answered a ping. peerMgrPong :: (MonadIO m) => PeerMgr -> Peer -> Word64 -> m () peerMgrPong mgr p nonce = PeerPong p nonce `send` mgr.mailbox +-- | Tell the peer manager about addresses relayed by a peer. They are added to+-- the known addresses if discovery is on and they are routable. peerMgrAddrs :: (MonadIO m) => PeerMgr -> Peer -> [NetworkAddress] -> m () peerMgrAddrs mgr p addrs = PeerAddrs p addrs `send` mgr.mailbox @@ -585,6 +746,7 @@       | otherwise -> True isRoutable _ = False +-- | Whether an IPv4 address is public unicast. routable4 :: (Word8, Word8, Word8, Word8) -> Bool routable4 (a, b, c, _)   | a == 0 = False -- this network@@ -602,6 +764,8 @@   | a >= 224 = False -- multicast and reserved   | otherwise = True +-- | Split a peer address into host and port. Accepts @host@, @host:port@,+-- @[ipv6]@ and @[ipv6]:port@. Either part is 'Nothing' if missing. toHostService :: String -> (Maybe String, Maybe String) toHostService str =   let host = case m6 of@@ -626,6 +790,9 @@         _ -> Nothing    in (host, srv) +-- | Resolve a peer address given as in 'toHostService' into socket addresses,+-- using the default port of the network if none is given. Returns an empty list+-- if it cannot be resolved. toSockAddr :: Network -> String -> IO [SockAddr] toSockAddr net str =   go `catch` e@@ -637,6 +804,7 @@     e :: (Monad m) => SomeException -> m [SockAddr]     e _ = return [] +-- | Median of a list, or 'Nothing' if it is empty. median :: (Ord a, Fractional a) => [a] -> Maybe a median ls   | null ls =@@ -649,6 +817,9 @@   where     ls' = sort ls +-- | Build our version message from the network, a random nonce, the best block+-- height, our address, the remote address and the current time in seconds since+-- the Unix epoch. buildVersion ::   Network ->   Word64 ->@@ -673,5 +844,6 @@       relay = True     } +-- | Protocol version that the node speaks. myVersion :: Word32 myVersion = 70012