# Haskoin Node
[](https://github.com/jprupp/haskoin-node/actions/workflows/ci.yml)
[](https://hackage.haskell.org/package/haskoin-node)
[](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,
a payload costs time linear in its size however it is split, anything
invalid ends the connection, a block must hold the transactions its header
commits to, relayed addresses are filtered and spread over network groups,
and the node reads a peer only as fast as it handles its messages. Your
subscribers are not throttled: one that falls behind holds memory.
- **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.
- **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 in `IO`, with no monad transformers and
no `MonadIO` or `MonadUnliftIO` constraints. 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 with `liftIO`.
- **Keep moving.** Every hint that a peer has blocks the node lacks makes it
ask that peer for headers, at once even while another peer syncs, and a
quiet period makes it ask every peer, so a missed announcement cannot leave
the chain behind. A syncing peer loses its turn when it sends nothing new,
and when it runs past the deadline that Bitcoin Core gives its initial
headers sync.
## 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, random choice among learned addresses with one connection per network group |
| Header sync | Headers downloaded from one peer at a time, with a deadline per turn, connected to the chain with the most work, stored in RocksDB; new blocks followed whether peers announce them with headers or inventory, announcing peers asked at once, 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; every block checked against its header's Merkle root and witness commitment |
| Protection | Strict parsing, per-command size limits, linear-time reads, back-pressure, bounded address pool, explicit handshake states, connect timeout |
### 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.5.0
- haskoin-core-1.6.1
- secp256k1-haskell-2.0.1
- base16-2.0
- rocksdb-haskell-jprupp-2.7.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 DisambiguateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
import Control.Monad (forever)
import Data.Default (def)
import Database.RocksDB (Config (..), withDB)
import Haskoin (BlockNode (..), 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`. Besides the imports of the quick start, the watcher needs
`Block (..)`, `Network` and `headerHash` from `Haskoin`, and `Inbox` from `NQE`:
```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. Configured peers are tried first; learned
addresses are picked at random, at most one per network group. 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, at once if
another peer holds the turn, and asks every peer when it has heard nothing
for `timeout` seconds. A turn ends when the peer has nothing more, sends a
full batch with nothing new, or runs past its deadline.
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.
`Haskoin.Node.Block` is a pure check of another kind: whether the
transactions of a block are the ones its header commits to. Every block a peer
sends passes through it.
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)` | An online peer sent a message: `MInv`, `MBlock`, `MTx` and so on |
Messages are published only from peers that are online, except `version` and
`verack`, which are the handshake itself. A block in an `MBlock` message has
passed `blockMutation`: its transactions hash to the Merkle root of its header,
and its witness data matches its coinbase when the node knows its parent.
Nothing else about it is checked, and a transaction in an `MTx` message is not
checked at all.
"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. The node does not wait for your
subscriber, though: a subscription is an unbounded queue, and one that falls
behind holds every event it has not read. Keep reading events, and hand slow
work to another thread.
### 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
```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. Unrelated messages that the
peer sends in between are ignored. `pingPeer` measures a round trip.
A block that `getBlocks` returns holds the transactions its header commits to.
A peer that sends another block under the requested header is disconnected,
and the request gives `Nothing` when it times out. The transactions are not
validated otherwise. A transaction from `getTxs` is matched by its id, which
does not cover its witness, so treat its witness data as unauthenticated until
you find the transaction in a block.
`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:
```haskell
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
```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
```
`chainRunHeaders` 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.
Addresses from DNS seeds must be public, like relayed addresses. On regtest the
seed of haskoin-core is `localhost`, which is not, so list local peers in
`peers` instead of relying on discovery.
### Custom connections
`connect` decides how to open a connection. `withConnection` opens a plain TCP
socket, and gives up if it does not open within five seconds (`connectTimeout`,
as in Bitcoin Core). 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` |
| Blocks | `blockMutation`, `BlockMutation`, `witnessCheck`, `WitnessCheck`, `segWitHeight` |
| 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.Block` | Whether a block holds the transactions its header commits to |
| `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, network groups and opening connections |
| `Haskoin.Node.Logger` | Logging |
## Security notes
- Every message header is checked before its payload is read. The payload limit
is the `maxPayloadSize` of the command on the network: 4,000,000 bytes on
Bitcoin, and on Bitcoin Cash 2 MiB for everything but block messages.
- Reading a payload costs time and memory linear in its size, however small
the pieces it arrives in.
- List messages are refused as soon as their count is over the limit, such as
2000 headers, 1000 addresses, 50000 inventory entries or 101 locator hashes.
- A peer that sends anything invalid is disconnected, with a `PeerException`
saying why.
- A peer that sends a block whose transactions are not the ones its header
commits to is disconnected (`MutatedBlock`). The check is Bitcoin Core's
`IsBlockMutated`: the Merkle root, a tree that repeats transactions
(CVE-2012-2459), and, once SegWit is active at the block's height, the
witness commitment. The header itself is not marked invalid. When the node
does not know the parent of a block, its witness data is not checked.
- Messages from a peer that has not completed the handshake are not acted on
or published, except the handshake messages.
- On Bitcoin, a peer must offer witness data (`NODE_WITNESS`) as well as the
full chain, as Bitcoin Core requires of its outbound peers.
- A peer that sends a second version or verack, or does not complete the
handshake within `timeout` seconds 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.
- A syncing peer whose full batch of headers brings nothing new loses its
turn. One that is still syncing after 15 minutes plus 1 millisecond for each
block expected since the best header is disconnected when another peer can
take over, as Bitcoin Core does.
- Addresses relayed by peers and returned by DNS seeds are only used if they
are public unicast addresses, so a peer or a resolver cannot make the node
connect to services on its own host or network. Static peers are not checked.
- Learned addresses are picked at random, and at most one connection is open to
each network group (a /16 of IPv4, a /32 of IPv6), so one operator cannot take
every slot. Static peers are exempt.
- At most 4096 relayed addresses and 4096 seed addresses are kept, apart, so
gossip cannot crowd out the seeds. When the room for one is full, a new
address replaces one that has failed to connect.
- A connection that does not open within five seconds is given up.
- 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.
- Below that work, during the initial sync, every valid header is stored. A
peer can make the node store a fork of low difficulty, at the cost of its
proof of work. Bitcoin Core first checks that a peer's headers reach the
minimum work before it stores any; this node does not yet.
- The node trusts the chain with the most work. It checks that a block holds
the transactions its header commits to, but does not validate the block:
check the transactions you get from peers before relying on them.
## 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 package also builds with [Cabal](https://www.haskell.org/cabal)
(`cabal build`, `cabal test`).
There are two test suites, using [hspec](https://hspec.github.io/) and
[QuickCheck](https://hackage.haskell.org/package/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.
```sh
stack test haskoin-node:spec
```
- `regtest` (`test/regtest`) runs the node against
[Bitcoin Core](https://bitcoincore.org) and
[Bitcoin Cash Node](https://bitcoincashnode.org) in regression test mode. The
nodes run in a [Podman](https://podman.io) container built from
`test/regtest/Containerfile`, whose script `test/regtest/install-nodes.sh`
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 or `HASKOIN_NODE_SKIP_REGTEST` is set.
```sh
stack test haskoin-node:regtest
```
Set `HASKOIN_TEST_LOG` to see the node's logs while tests run.
CI builds and tests every change with the OS libraries, with `stack --nix`,
and against the regtest nodes. Its steps call the scripts in `scripts/`, which
you can also run locally: `scripts/check-local-deps.sh` fails if `stack.yaml`
points at a package outside the repository, which CI cannot build, and
`scripts/install-system-libraries.sh` installs the C libraries on Ubuntu.
### Contributing
Pull requests are welcome.
- Format the code with [Ormolu](https://github.com/tweag/ormolu).
- Add tests for the behaviour you change.
- Keep lines of code, including scripts, workflows and the examples in the
documentation, shorter than 120 columns.
- Put multi-line scripts in their own files rather than embedding them in a
workflow or a Containerfile.
- 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).