haskoin-node
P2P library for Bitcoin and Bitcoin Cash
https://github.com/jprupp/haskoin-node#readme
| LTS Haskell 23.28: | 1.1.4 |
| Stackage Nightly 2026-11-10: | 2.4.0 |
| Latest on Hackage: | 2.4.0 |
haskoin-node-2.4.0@sha256:e5d00163b8888a3f1614d11765b6f54d74ef1d5dc5f9ad87e2052a983bc0f35b,4567Module documentation for 2.4.0
Haskoin Node
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 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.
Contents
- Why Haskoin Node
- Design principles
- Features
- Installation
- Quick start
- Tutorial: a block watcher
- Concepts
- Usage guide
- Configuration
- Module overview
- Security notes
- Development
- Related projects
- 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, 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 processes with their own state. One misbehaving peer dies alone.
- Explicit protocol states. What the node expects from each peer, during the handshake, while syncing headers and while receiving requested data, is written down as a pure state machine. A message that does not fit the current state is refused instead of handled by accident, and every rule is tested without a network.
- Plain
IO. Every function runs inIO, with no monad transformers and noMonadIOorMonadUnliftIOconstraints. Types say exactly what runs, GHC has nothing to specialise, and long-running loops cannot leak through an unspecialised dictionary. From your own monad, call the node withliftIO. - Never stuck. Every hint that a peer has blocks the node lacks makes it ask for headers, and a quiet period makes it ask every peer, so a missed announcement cannot leave the chain behind.
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, retry backoff and bans for bad addresses |
| Header sync | Headers downloaded from one peer at a time, connected to the chain with the most work, stored in RocksDB; new blocks followed whether peers announce them with headers or inventory, and peers polled after a quiet period |
| 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, explicit handshake states |
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 and
libsecp256k1 0.2.0 or later, with
pkg-config to find them.
# 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 for how to build it.
Check the libraries are found
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
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:
extra-deps:
- haskoin-node-2.4.0
- haskoin-core-1.6.1
- secp256k1-haskell-2.0.1
- base16-2.0
- rocksdb-haskell-jprupp-2.6.1
- rocksdb-query-0.6.1
- nqe-0.6.9
Step by step with Stack
-
Install Stack and the libraries above.
-
Create a project:
stack new node-demo simple cd node-demo -
Add the packages listed under With Cabal to
build-dependsinnode-demo.cabal, plusnetwork. -
If your resolver is older than the packages, add the
extra-depsshown under With Stack tostack.yaml. -
Put the quick start program in
src/Main.hsand run it:stack runWithin 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:
{-# 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:
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:
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:
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, and run the watcher
inside withNode:
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:
- 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. An address whose connection failed is tried again later each time, and one whose peer misbehaved waits a day. - 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. It asks a peer for headers whenever the peer announces blocks it does not know, with headers or with inventory, and asks every peer when it has heard nothing fortimeoutseconds.
The rules for talking to peers are pure state machines, each in its own module:
| Module | Rules |
|---|---|
Haskoin.Node.PeerMgr.Lifecycle |
Life of a connection: handshake, pings, timers, disconnection |
Haskoin.Node.PeerMgr.Handshake |
The version handshake, the first stage of a connection |
Haskoin.Node.PeerMgr.Addresses |
Which address to try next, and when to retry one |
Haskoin.Node.Chain.Sync |
Which peer to ask for headers, and when headers count as synced |
Haskoin.Node.Peer.Request |
Receiving requested blocks and transactions |
Haskoin.Node.Peer.Lock |
Who may hold a peer for a long conversation |
The processes store the state and carry out what a transition says. Time is an input to the transitions, so every timing rule is tested without waiting.
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. Haskoin.Node.Chain.Database has
the code.
Usage guide
Picking a peer and requesting data
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. Unrelated messages that the
peer sends in between are ignored. pingPeer measures a round trip.
getPeers leaves out peers that are being disconnected, even before their
connection closes.
For a conversation that spans many messages, such as downloading a long run of blocks, reserve the peer first so that no other task uses it at the same time. The header chain does the same while it syncs. Only the owner that reserved a peer can release it:
owner <- newOwner "downloader"
reserved <- reservePeer owner p
when reserved $ do
-- ... long conversation with p ...
void (releasePeer owner p)
setBusy and setFree still work, but are deprecated: they act for a single
owner shared by all their callers.
Querying headers
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
chainRunHeaders runs the header functions of haskoin-core, such as getAncestor,
against the database of the chain.
Static peers
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, without a pong, or without completing the handshake, before a peer is dropped; also how long the syncing peer may take to send headers, and the quiet period after which every peer is asked for headers |
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 re-exports everything a program needs:
| Part | Contents |
|---|---|
| Node | NodeConfig, NodeEvent, Node, withNode, withConnection |
| Peers | Peer, PeerEvent, PeerException, sendMessage, getBlocks, getTxs, getData, pingPeer, killPeer, newOwner, reservePeer, releasePeer |
| Peer manager | PeerMgr, OnlinePeer, PeerState, Stage, DisconnectReason, getPeers, getOnlinePeer, toSockAddr, isRoutable |
| Chain | Chain, ChainEvent, chainGetBest, chainGetBlock, chainGetAncestor, chainGetParents, chainGetSplitBlock, chainBlockMain, chainIsSynced, chainRunHeaders |
| 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.
Every module is exposed and can be imported on its own:
| Module | Purpose |
|---|---|
Haskoin.Node.Peer |
The process that talks to one peer, and data requests |
Haskoin.Node.Peer.Request |
State machine for receiving requested blocks and transactions |
Haskoin.Node.Peer.Lock |
State machine for reserving a peer |
Haskoin.Node.PeerMgr |
Finds, connects, ranks and drops peers |
Haskoin.Node.PeerMgr.Lifecycle |
State machine for the life of a connection |
Haskoin.Node.PeerMgr.Handshake |
State machine for the version handshake |
Haskoin.Node.PeerMgr.Addresses |
State machine for the address book |
Haskoin.Node.Chain |
Keeps the header chain in sync |
Haskoin.Node.Chain.Sync |
State machine that decides which peer to ask for headers, and when the chain is synced |
Haskoin.Node.Chain.Database |
Storage of headers in RocksDB |
Haskoin.Node.Address |
Parsing peer addresses and opening connections |
Haskoin.Node.Logger |
Logging |
Security notes
- Every message header is checked before its payload is read. The payload limit
is the
maxPayloadSizeof 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, with a
PeerExceptionsaying why. - A peer that sends a second version or verack, or does not complete the
handshake within
timeoutseconds even while sending other messages, is disconnected. - A peer that sends invalid headers, or does not answer a request for headers in time, is never asked for headers again before it disconnects.
- 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.
- An address whose connection fails waits twice as long after each failure before it is tried again, up to ten minutes. A relayed address whose peer broke the protocol is not tried again for a day. Static peers are never banned, and wait at most ten minutes.
- 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
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 generates.
The package also builds with Cabal
(cabal build, cabal test).
There are two test suites, using hspec and QuickCheck:
-
spec(test/unit) tests the state machines with properties, and runs the peer, peer manager, chain and node against in-memory mock peers. It needs no Internet access.stack test haskoin-node:spec -
regtest(test/regtest) runs the node against Bitcoin Core and Bitcoin Cash Node in regression test mode. The nodes run in a Podman container built fromtest/regtest/Containerfile, which installs the official release binaries on Ubuntu after checking their SHA-256 hashes. The image is built the first time the suite runs. Nodes use random ports, so runs do not collide. The suite is skipped if Podman is missing orHASKOIN_NODE_SKIP_REGTESTis set.stack test haskoin-node:regtest
Set HASKOIN_TEST_LOG to see the node’s logs while tests run.
Contributing
Pull requests are welcome.
- Format the code with Ormolu.
- Add tests for the behaviour you change.
- Describe the change under a new version in
CHANGELOG.md. The file follows Keep a Changelog, and versions follow Semantic Versioning.
Related projects
- haskoin-core has the Bitcoin data types and protocol that this library speaks.
- haskoin-store indexes blocks and transactions on top of this node and serves them over a REST API.
- nqe provides the processes, supervisors and publishers.
- rocksdb-query stores the headers.
License
Haskoin Node is released under the MIT License.
Changes
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[2.4.0] - 2026-11-09
Added
chainBlocksAnnounced: the node passes blocks announced in inventory messages to the chain, which asks the peer for headers if it does not know them.- Pure state machines, each with property tests: the life of a connection
(
Haskoin.Node.PeerMgr.Lifecycle) and its version handshake (Haskoin.Node.PeerMgr.Handshake), the address book (Haskoin.Node.PeerMgr.Addresses), reserving a peer (Haskoin.Node.Peer.Lock), choosing which peer to ask for headers (Haskoin.Node.Chain.Sync), and receiving requested blocks and transactions (Haskoin.Node.Peer.Request). isSyncedsays whether the header chain counts as synced, as a pure function.newOwner,reservePeer,releasePeerandpeerHolderreserve a peer for an owner, so that only that owner can release it.PeerExceptionsays why a peer connection ended, instead of anerrormessage.Haskoin.Node.AddressandHaskoin.Node.Chain.Databasehold code split out of the peer manager, the node and the chain, which still re-export it. Every module is exposed, andPeerMessageis exported.- Unit tests of the chain, peer and peer manager processes against in-memory mock peers.
- A regtest suite that runs the node against Bitcoin Core 32.1 and Bitcoin Cash Node 29.2.0 in Podman, from official binaries with checked hashes, and a CI job that runs it.
cabal.project, so the package builds with Cabal too.chainRunHeadersruns header functions of haskoin-core, such asgetAncestororblockLocator, against the headers of a chain, andrunHeadersdoes the same for aChainDB.
Changed
- If no headers are requested or received for
timeoutseconds, every connected peer is asked for headers again. This bounds how long any stall of the header chain can last. - The chain checks for timeouts at intervals of at most a quarter of
timeout, between 0.1 and 20 seconds, instead of every 2 to 20 seconds. - Breaking:
OnlinePeerholds the state of the connection in astatefield instead of theverack,online,version,ping,pings,connectedandtickledfields. These are still available as computed fields, soo.versionando.onlinekeep working, but code that builds or pattern matchesOnlinePeerrecords must change. - Breaking: the busy flag of a
Peeris a lock held by an owner, sowrapPeertakes aTVar (Lock Owner).setBusyandsetFreeare deprecated: they act for a single owner shared by all their callers, andsetFreeno longer releases a peer that another owner reserved. The chain reserves peers as its own owner. - A peer told to disconnect is no longer returned by
getPeers, and its messages are no longer routed, even before its connection closes. - A peer that does not complete the handshake within
timeoutseconds is disconnected, even if it keeps sending other messages. - An address whose connection fails waits before it is tried again, twice as long after each failure up to ten minutes, and a relayed address whose peer broke the protocol waits a day. Before, failing addresses were retried within seconds forever. The configured peers and seeds are resolved again at most every five minutes.
- Breaking:
ChainTandrunChainTare removed, along with theBlockHeadersinstances forReaderT Chain mandReaderT ChainConfig m. UsechainRunHeadersinstead. The package no longer uses monad transformers outsideHaskoin.Node.Chain.Database, where aReaderTcarries the database for haskoin-core’s header functions;withNodenests its resources directly instead of throughContT. - Breaking: every function runs in
IO. TheMonadIOandMonadUnliftIOconstraints are gone, so code in another monad must call the node withliftIO. The library depends on async and stm instead of unliftio. - The package uses the GHC2024 language edition, strict warnings and upper
bounds on its dependencies.
Setup.hs,hie.yamland.hindent.yamlwere removed. stack.yamlbuilds against secp256k1-haskell 2.0.1. The README’s Stack example names the same version.
Fixed
- Peers were ranked by the newest ping and the ten fastest ever measured, instead of the last eleven: slow recent pings were dropped. A peer that became slow kept ranking as fast.
- The header chain stopped following the network when a peer announced new blocks with an inventory message instead of headers, which peers do when many blocks are found at once. The chain stayed at the old height until the peer reconnected.
- A peer that announced headers that did not connect to the chain was disconnected. It is now asked for headers.
- A syncing peer that timed out kept the chain waiting until its disconnection arrived. The chain now moves on at once.
- A full batch of headers from a peer that was not being synced against was not followed up.
getData,getBlocksandgetTxsfailed when the peer sent an unrelated message, or a notfound message for items that were not requested, before all the requested items arrived.
[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
- This package requires haskoin-core 1.6.0, the release with the fixes of the November 2026 security review.
- Breaking: the
version,timestampandstartHeightfields of theVersionof anOnlinePeerare signed numbers, as haskoin-core 1.6.0 reads them.buildVersiontakes the same arguments as before. - The headers are not reported as synced before the best header has the
minimumChainWorkof the network. This holds for theChainSyncedevent and forchainIsSynced. A recent header on a chain with less work used to be enough. - Once the best header has the
minimumChainWorkof the network, a peer whose headers fork from far below the tip, on a chain with less work, is disconnected. haskoin-core 1.6.0 refuses such headers. - A peer that sends a header the node already has at or below a checkpoint is no longer disconnected. haskoin-core 1.6.0 returns the stored header.
- Transactions from Bitcoin Cash peers are read in the format without witness only, as haskoin-core 1.6.0 reads them on a network without SegWit.
- The log line for an invalid payload gives the reason the parser refused it.
- The payload size limit is the
maxPayloadSizeof the command on the network instead of the network’smaxMessageSize. On Bitcoin Cash that is 2 MiB for every message butblock,cmpctblockandblocktxn, which may be twice the block size.
Fixed
- Peers that send more than 50000 entries in an
invorgetdatamessage, or a block locator of more than 101 hashes, are disconnected. haskoin-core 1.6.0 refuses such a message as soon as it reads the count.
[2.2.0] - 2026-11-04
Changed
- Breaking:
Peerhas apendingfield,wrapPeertakes itsTVar Int, andpeertakes thePeerthatwrapPeerbuilt instead of the busyTVar. - Messages from a peer apply backpressure. A peer may have at most
maxPendingMessagesmessages waiting for the node; after that the node stops reading its socket until it catches up. Address, header, version, verack, ping and pong messages are handled to completion before the next one is taken. A fast peer could fill the node’s unbounded queues before. - Outgoing messages are logged by command at debug level, not in full.
Fixed
- Bitcoin Cash headers sync again. This package now requires haskoin-core
1.5.1, which evaluates the ASERT difficulty rule at the parent block. With
haskoin-core 1.5.0 the node stopped at mainnet block 975899 with
Incorrect difficulty bits, because that release checked the bits of each header with a formula that used the header’s own timestamp. - Network magic and payload size are checked right after the message header,
before the payload is read. The size limit is the network’s
maxMessageSizeinstead of a fixed 32 MiB. An oversized message used to be logged and its payload then parsed as the next header. - Peers that send more than 2000 headers or more than 1000 addresses in one message are disconnected before the message is decoded.
- Address and header messages from a peer are ignored until it completes the handshake.
- A second version or verack from a peer disconnects it. Each one used to announce the peer as connected again.
- Relayed addresses that are not public unicast addresses (loopback,
private, link-local, multicast, documentation and similar ranges, or port
0) are not added to the address pool.
isRoutableis exported. - A peer that does not answer a ping within the timeout is disconnected, even if it keeps sending other messages.
- Only headers from the syncing peer reset its sync timeout. Another peer’s headers could keep a silent syncing peer from ever timing out.
- The peer manager rechecks
maxPeerswhen it handles a queued connection, so the connect loop can no longer go over the limit while the manager is busy. - Purging an old chain database with a column family deleted the same keys from the default column family too. The purge now deletes keys as it walks them instead of building a list of all of them in memory.
[2.1.2] - 2026-11-04
Fixed
- An empty headers message from the syncing peer ends the header sync. The node used to take it as a reason to ask the same peer again, and kept sending getheaders in a tight loop, thousands per second, whenever its best header was the peer’s tip after a full batch of 2000.
[2.1.1] - 2026-11-04
Changed
- The loop that parses messages from a peer uses
loopForevertoo, like every other long-running loop. - Tests use IPv4 only: the mock peer is at
127.0.0.1instead of[::1], and the address property test generates IPv4 addresses. On a host without IPv6 the mock peer could not be resolved and the peer and chain tests hung.
[2.1.0] - 2026-11-03
- Update to haskoin-core-1.5.0.
[2.0.0] - 2026-10-31
Changed
- Replace monad-logger with fast-logger. Configuration records take a
Logger, and functions need onlyMonadIOorMonadUnliftIO. - Require haskoin-core 1.5.0, whose
buildMerkleRootreturnsEither String MerkleRoot.
[1.4.4] - 2026-08-31
Added
- Log
connectBlockserror message.
Changed
- Peer connection logged incorrectly as warn is now logged as info.
Fixed
- Correct documentation for
importHeadersfunction.
[1.4.3] - 2026-08-13
Fixed
- Fix regression with peer disconnection.
[1.4.2] - 2026-08-13
Fixed
- Fix regression with peer selection.
[1.4.1] - 2026-08-13
Removed
- Remove PeerException type which is no longer used.
[1.4.0] - 2026-08-13
Changed
- Add monad transformers and monad-logger again because logging is too slow.
[1.3.1] - 2026-08-12
Added
- Add monad transformers to support haskoin-store.
[1.3.0] - 2026-08-12
Changed
- Simplify modules to make compatible with upstream changes.
[1.2.0] - 2026-08-12
Changed
- Upstream changes on multiple libraries.
[1.1.4] - 2025-05-08
Changed
- Move to personal repository.
[1.1.3] - 2024-05-23
Fixed
- Tickle a peer whenever a message is received.
[1.1.2] - 2024-05-23
Fixed
- Correct log messages from peer manager.
[1.1.1] - 2024-05-23
Fixed
- Peers timeout more aggressively.
[1.1.0] - 2024-03-14
Changed
- Update LTS Haskell & upstream dependencies.
[1.0.1] - 2023-08-03
Changed
- Add compatibility with latest LTS Haskell.
[1.0.0] - 2023-07-28
Changed
- Make compatible with latest haskoin-core.
- Use DuplicateRecordFields and OverloadedRecordDot.
- Simplify pub/sub queues.
- Multiple refactoring passes.
[0.18.1] - 2022-07-27
Fixed
- Set default port for peers where it is unset.
[0.18.0] - 2022-07-27
Added
- Support setting up and connecting to IPv6 peers.
[0.17.14] - 2021-08-14
Fixed
- Reduce verbosity on incoming header decode test.
- Show appropriate error message upon receiving empty headers.
[0.17.13] - 2021-08-14
Added
- Display more details about invalid incoming headers.
[0.17.12] - 2021-05-17
Fixed
- Do not connect to more than the maximum number of peers.
[0.17.11] - 2021-05-17
Added
- Display message command that disconnects a peer.
[0.17.10] - 2021-05-17
Fixed
- Correct disconnect timeout algorithm bug.
[0.17.9] - 2021-05-17
Fixed
- Add randomised timeouts to avoid disconnecting all peers.
[0.17.2] - 2021-03-09
Fixed
- Do not start chain actor until database initialized.
[0.17.1] - 2021-01-08
Changed
- Depend on haskoin-core-0.17.3.
[0.17.0] - 2020-10-21
Added
- Support for Bitcoin Cash November 2020 hard fork.
- BlockHeaders instance for ReaderT Chain.
[0.16.0] - 2020-07-23
Changed
- Add support for column families.
[0.15.0] - 2020-07-20
Changed
- Use new Haskell bindings for RocksDB.
[0.14.1] - 2020-06-19
Fixed
- Correct flawed peer locking logic in Chain actor.
[0.14.0] - 2020-06-18
Changed
- Massively refactor everything in a non-backwards-compatible way.
- Use MIT license.
- Bump haskoin-core.
- Bump secp256k1-haskell.
Fixed
- Fix getting stuck on a single peer.
[0.13.0] - 2020-05-08
Changed
- Depend on Haskoin Store 0.13.3.
- Better code organisation.
[0.12.0] - 2020-05-06
Changed
- Add a test suite that simulates network instead of connecting to real one.
[0.11.3] - 2020-05-03
Changed
- Revert including multiline decoding error text in logs.
[0.11.2] - 2020-05-03
Changed
- Include header decoding error text in logs.
[0.11.1] - 2020-05-03
Changed
- Improve logging.
[0.11.0] - 2020-05-03
Changed
- Set peer too old time
[0.10.1] - 2020-05-03
Changed
- Disconnect old peers after 48 hours instead of 30 minutes.
[0.10.0] - 2020-05-03
Changed
- Move modules out of Network.Haskoin namespace.
- Add better and more logging.
- Change Manager module name and related values to PeerManager.
[0.9.21] - 2020-04-07
Removed
- Remove unnecessary logging.
[0.9.20] - 2020-04-07
Changed
- Better log messages.
- Less verbose debug logging.
[0.9.19] - 2020-04-07
Changed
- Better log messages.
[0.9.18] - 2020-04-07
Added
- More aggressive peer discovery.
[0.9.17] - 2020-04-07
Added
- Peers are disconnected automatically after awhile.
[0.9.16] - 2020-02-08
Added
- Lower bound versions for some dependencies.
[0.9.15] - 2020-01-15
Changed
- Update to support new
NetworkAddressdata structure fromhaskoin-core.
[0.9.14] - 2019-12-10
Removed
- No longer support storing peers in db as performance tradeoff doesn’t justify it.
[0.9.13] - 2019-10-08
Changed
- Really store peers in db.
[0.9.12] - 2019-10-08
Changed
- Demote some logging to debug level.
[0.9.11] - 2019-10-02
Added
- Add
-O2optimisations to GHC.
[0.9.10] - 2019-04-19
Added
- Increase debugging information where application freezes.
[0.9.9] - 2019-04-12
Added
- Increase debugging information.
[0.9.8] - 2019-04-12
Changed
- Increase version of haskoin-core to 0.9.0.
- Fix some tests.
[0.9.7] - 2019-04-12
Added
- More debugging.
Changed
- Be defensive against duplicate peers.
- Increase interval between housekeeping pings.
- Replace peers in database atomically.
[0.9.6] - 2019-04-01
Changed
- Randomize known peers instead of keeping scores.
- Simplify peer management code to avoid freezes.
- Merge logic for chain and manager.
[0.9.5] - 2018-11-14
Changed
- Do not record new peers in database when peer discovery is disabled.
[0.9.4] - 2018-11-01
Changed
- Don’t spam best block events.
[0.9.3] - 2018-10-22
Changed
- Correct display of milliseconds in log.
- Correct bug when receiving headers from unknown peer.
- Simplify chain syncing code.
[0.9.2] - 2018-10-18
Changed
- Peer dies immediately when receiving a bad message.
[0.9.1] - 2018-10-18
Changed
- Keep track of last synced header from a peer to avoid endless loops on large reorgs.
[0.9.0] - 2018-10-17
Changed
- Use an STM listener instead of a publisher for the node API.
[0.8.2] - 2018-10-17
Added
- Expose
ChainMessageandManagerMessagetypes fromHaskoin.Nodemodule.
[0.8.1] - 2018-10-11
Changed
- Corrected documentation for
killPeerfunction. - Leave time out of logic code.
[0.8.0] - 2018-10-09
Changed
- Peers are now killed directly instead of through peer manager.
Removed
- Chain no longer needs peer manager.
[0.7.2] - 2018-10-09
Added
- Compatibility with base 4.12.
Changed
- Update base to 4.9.
[0.7.1] - 2018-10-09
Added
- Allow to easily obtain a peer’s publisher.
[0.7.0] - 2018-10-09
Added
- Versioning for chain and peer database.
- Automatic purging of chain and peer database when version changes.
- Add extra timers.
- Add publishers to every peer.
Changed
- Full reimplementation of node API.
- Simplify peer selection and management.
- Merge manager and peer events.
- Rename configuration variables for node.
- Separate logic from actors for peer manager and chain.
Removed
- Remove irrelevant fields from peer information.
- Remove unreliable peer block head tracking.
- Remove dependency on deprecated binary conduits.
- Remove Bloom filter support from manager.
- Remove unreliable peer request tracking code.
- Remove separate manager events.
[0.6.1] - 2018-09-14
Changed
- Fix bug where peer height did not update in certain cases.
[0.6.0] - 2018-09-14
Added
- Documentation everywhere.
Changed
- Make compatible with NQE 0.5.
- Use supervisor only in peer manager.
- API quality of life changes.
- Exposed module is now only
Haskoin.Node.
Removed
- No more direct access to internals.
[0.5.2] - 2018-09-10
Changed
- Improve dependency definitions.
[0.5.1] - 2018-09-10
Changed
- Dependency
sec256k1changes tosecp256k1-haskell.
[0.5.0] - 2018-09-09
Added
- New
CHANGELOG.mdfile. - Use
nqefor concurrency. - Peer discovery.
- RocksDB peer and block header storage.
- Support for Merkle blocks.
Changed
- Split out of former
haskoinrepository. - Use hpack and
package.yaml. - Old
haskoin-nodepackage now renamed toold-haskoin-nodeand deprecated.
Removed
- Removed Old Haskoin Node package completely.
- Removed Stylish Haskell configuration file.
- Remvoed
haskoin-coreandhaskoin-walletpackages from this repository.