Running a Bitcoin full node is a great way to ensure the health and integrity of the decentralized Bitcoin network. This blog post is meant to be a guide for compiling, running, and monitoring a Bitcoin full node on a server, and as such won't delve too much into the specifics as to what the node does, or why it's important to the network health. If you want to know more check out 6 Reasons To Run a Bitcoin Full Node.

getblockchaininfo

Put simply, running a full node will add to the large number of full nodes running across the world to support the Bitcoin network. The nodes ensure the rules of the protocol and consensus algorithms in place are upheld and enforced, which is what fundamentally allows Bitcoin to work and function the way it should.

Miners create new blocks and submit them to the network for verification - it's the job of the full nodes to verify these blocks (groups of transactions) and ultimately accept or reject them based on their validity. Every full node will verify the validity of any and all blocks submitted to the network and, for each block, decide if it will be the next new block in the chain or whether it should be thrown out in the case that it is invalid (by either a bug in the mining software or a bad actor trying to undermine the network). Full nodes are the final arbiters when it comes to determining which transactions are valid or invalid.

Provision the VM

I provisioned a fresh SmartOS 16Q4 LTS instance with the following JSON payload:

~/crypto.json

{
  "brand": "joyent",
  "image_uuid": "1f32508c-e6e9-11e6-bc05-8fea9e979940",
  "autoboot": true,
  "alias": "crypto",
  "hostname": "crypto.rapture.com",
  "dns_domain": "rapture.com",
  "quota": 500,
  "resolvers": [
    "10.0.1.2",
    "10.0.1.3"
  ],
  "ram": 2048,
  "nics": [
    {
      "nic_tag": "admin",
      "ip": "10.0.1.38",
      "netmask": "255.255.255.0",
      "gateway": "10.0.1.1",
      "primary": true
    }
  ]
}

At the time of this post, the Bitcoin blockchain is ~250GB, so ensure the quota is set to something large enough to store all of the data.

I created the VM (in the global zone) using the above JSON payload with:

vmadm create -f crypto.json

I then logged into the VM and created a bitcoin group and user to use:

groupadd bitcoin
useradd -g bitcoin -s /bin/bash -m bitcoin
su - bitcoin

Installation

With the VM provisioned, and being logged in as the bitcoin user, the bitcoind program can now be compiled and installed.

First, install the dependencies:

sudo pkgin in build-essential autoconf pkg-config boost

Pull the source and check out the latest version (v0.17.1 at the time of this writing):

cd ~
git clone https://github.com/bitcoin/bitcoin.git
cd bitcoin/
git checkout v0.17.1

Patch the source to work on SmartOS. For more information see my comment on this bitcoin issue:

vim ./src/compat/glibc_sanity.cpp
$ git diff
diff --git a/src/compat/glibc_sanity.cpp b/src/compat/glibc_sanity.cpp
index 1ef66e27b..0166fea90 100644
--- a/src/compat/glibc_sanity.cpp
+++ b/src/compat/glibc_sanity.cpp
@@ -7,6 +7,7 @@
 #endif

 #include <cstddef>
+#include <cstring>

 #if defined(HAVE_SYS_SELECT_H)
 #include <sys/select.h>

With the patch in place, generate the Makefile with autogen and configure:

./autogen.sh
LIBS='-lsocket' ./configure \
    --disable-wallet \
    --without-gui \
    --without-miniupnpc \
    --disable-tests

Compile and install the program:

make
sudo make install

Finally, ensure the compilation worked with:

$ bitcoind -version
Bitcoin Core Daemon version v0.17.1.0-ef70f9b52-dirty
Copyright (C) 2009-2018 The Bitcoin Core developers
...

bitcoin service

Create a directory to store all of the Bitcoin related data files. You can use any directory here:

sudo mkdir /opt/custom/opt/bitcoind
sudo mkdir /opt/custom/opt/bitcoind/data
sudo chown -R bitcoin:bitcoin /opt/custom/opt/bitcoind

Create a bitcoin.conf configuration file:

/opt/custom/opt/bitcoind/bitcoin.conf

# general
testnet=0
datadir=/opt/custom/opt/bitcoind/data

# server
listen=1
bind=0.0.0.0:8333

# rpc
server=1
rpcbind=localhost:8332

This tells bitcoind to store the blockchain data in /opt/custom/opt/bitcoind/data and to listen locally for RPC connections on port 8332 and globally for p2p connections on port 8333.

Create a bitcoin SMF service using the following XML:

~/bitcoin.xml

<?xml version='1.0'?>
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
<service_bundle type='manifest' name='export'>
  <service name='application/bitcoin' type='service' version='0'>
    <create_default_instance enabled='true'/>
    <dependency name='dep0' grouping='require_all' restart_on='error' type='service'>
      <service_fmri value='svc:/milestone/multi-user:default'/>
    </dependency>
    <exec_method name='start' type='method' exec='bitcoind -conf=/opt/custom/opt/bitcoind/bitcoin.conf &amp;' timeout_seconds='10'>
      <method_context working_directory='/home/bitcoin'>
        <method_credential user='bitcoin' group='bitcoin'/>
        <method_environment>
          <envvar name='HOME' value='/home/bitcoin'/>
          <envvar name='PATH' value='/opt/local/sbin:/opt/local/bin:/opt/custom/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'/>
        </method_environment>
      </method_context>
    </exec_method>
    <exec_method name='stop' type='method' exec=':kill' timeout_seconds='30'/>
    <template>
      <common_name>
        <loctext xml:lang='C'>Bitcoin Full Node</loctext>
      </common_name>
    </template>
  </service>
</service_bundle>

Import the manifest with:

sudo svccfg import bitcoin.xml

Then, check on its status with:

$ svcs -p bitcoin
STATE          STIME    FMRI
online         Jan_16   svc:/application/bitcoin:default
               Jan_16      58818 bitcoind
$ svcs -L bitcoin
/var/svc/log/application-bitcoin:default.log

Also, the bitcoin-cli program can be used to talk to the bitcoind process over the RPC port. Because the datadir is not the default (~/.bitcoin) it must be specified as the -datadir argument:

$ sudo bitcoin-cli -datadir=/opt/custom/opt/bitcoind/data getblockchaininfo
{
  "chain": "main",
  "blocks": 559139,
  "headers": 559139,
  "bestblockhash": "0000000000000000001dcb21b7b2e067b27a6c8330681376ddfc6287d6d3e86c",
  "difficulty": 5883988430955.408,
  "mediantime": 1547885572,
  "verificationprogress": 0.9999953549007587,
  "initialblockdownload": false,
  "chainwork": "000000000000000000000000000000000000000004c516ec9378bdb4a348f8b4",
  "size_on_disk": 227961652232,
  "pruned": false,
  "softforks": [
    ...
  ],
  "bip9_softforks": {
    ...
  },
  "warnings": ""
}

In the output above, my node was already fully synchronized, but if it hadn't been, the verificationprogress would be far less than 0.999 (think of it like a percentage) and initialblockdownload would be true.

The command help can be used with bitcoin-cli to get a list of all possible commands the daemon supports.

Expose The Bitcoin Port

The final step is to forward port 8333 to the machine on your network running the Bitcoin full node. If the machine has a public IP address and is not sitting behind NAT, then this step can be skipped.

For me, all I had to do was map 8333 on my router to point to 10.0.1.38.

Verification

To verify that the node is up, running, and accessible to the outside world, I found bitnodes to be incredibly helpful.

Bitnodes shows all of the full nodes that it has discovered in the Bitcoin network, which at this time ~10,500 nodes. There is a "CHECK NODE" button on the site that should already have your public IP address and port number populated. Click that and make sure that the result is green with a success message.

Monitoring

I wrote 2 nagios checks for bitcoind to verify the service is up and running as expected.

$ ./check_bitcoin 10.0.1.38
ok: node (/Satoshi:0.17.1/ 70015) height 559199 blocks
$ ./check_bitcoind_version 10.0.1.38
ok: bitcoind running latest version v0.17.1

check_bitcoin

This script checks a Bitcoin node over the p2p port (8333 by default) to ensure that it is responsive and that it can respond to the command to get the version number.

I have this check scheduled to run every 15 minutes against my full node:

#!/usr/bin/env node
/*
 * Check bitcoin p2p port
 *
 * Author: Dave Eddy <dave@daveeddy.com>
 * Date: January 17, 2019
 * License: MIT
 */

var bitcoin = require('./lib/bitcoin');

var host = process.argv[2];
var port = +process.argv[3] || 8333;

if (!host) {
    console.log('unknown: host required as first argument');
    process.exit(3);
}

var obj = {
    host: host,
    port: port
};

bitcoin.getVersion(obj, function (err, peer) {
    if (err) {
        console.log('critical: %s', err.message);
        process.exit(2);
    }

    console.log('ok: node (%s %d) height %d blocks',
        peer.subversion, peer.version, peer.bestHeight);
});

check_bitcoind_version

This script checks a Bitcoin node over the p2p port (8333 by default) to gather the current version number (of the daemon that is running) and the latest version of bitcoind from GitHub, and compares the 2 values.

I have this check run every day against my full node with notifications disabled. This way, when I'm glancing at my nagios dashboard, I'll be able to see if there is a newer version of bitcoind to compile and install.

#!/usr/bin/env node
/*
 * Check bitcoin version against the latest on GitHub
 *
 * Author: Dave Eddy <dave@daveeddy.com>
 * Date: January 18, 2019
 * License: MIT
 */

var assert = require('assert-plus');
var request = require('request');

var bitcoin = require('./lib/bitcoin');

var host = process.argv[2];
var port = +process.argv[3] || 8333;

var uri = 'https://api.github.com/repos/bitcoin/bitcoin/releases';

if (!host) {
    console.log('unknown: host required as first argument');
    process.exit(3);
}

var obj = {
    host: host,
    port: port
};

bitcoin.getVersion(obj, function (err, peer) {
    if (err) {
        console.log('unknown: %s', err.message);
        process.exit(3);
    }

    // parse bitcoind version
    var match = peer.subversion.match(/^\/[^:]+:([^/]+)\/$/);
    if (!match) {
        console.log('unknown: failed to parse subversion "%s"',
            peer.subversion);
        process.exit(3);
    }
    var version = match[1];

    var opts = {
        uri: uri,
        json: true,
        headers: {
            'User-Agent': 'check_bitcoind_version nagios check'
        }
    };

    // request latest version from github
    request(opts, function (err, res, body) {
        if (err) {
            console.log('unknown: failed to get latest github version - %s',
                err.messsage);
            process.exit(3);
        }

        assert.arrayOfObject(body, 'body');

        // grab the latest version
        var latest = body[0].tag_name;

        if (!latest) {
            console.log('unknown: failed to parse github response');
            process.exit(3);
        }

        // normalize version numbers
        if (version[0] !== 'v')
            version = 'v' + version;
        if (latest[0] !== 'v')
            latest = 'v' + latest;

        if (version !== latest) {
            console.log('warning: bitcoind running version %s, latest is %s',
                version, latest);
            process.exit(1);
        }

        console.log('ok: bitcoind running latest version %s', version);
    });
});

./lib/bitcoin

Both nagios checks rely on the following small library I wrote to wrap the bitcore-p2p npm library.

/*
 * Stupidly simple bitcoin library
 *
 * Author: Dave Eddy <dave@daveeddy.com>
 * Date: January 17, 2019
 * License: MIT
 */

var assert = require('assert-plus');
var Peer = require('bitcore-p2p').Peer;

module.exports.getVersion = getVersion;

function getVersion(obj, cb) {
    assert.object(obj, 'obj');
    assert.func(cb, 'cb');

    var peer = new Peer(obj);

    var isOk = false;

    peer.on('ready', function () {
        isOk = true;
        peer.disconnect();
    });

    peer.on('disconnect', function () {
        if (isOk) {
            cb(null, peer);
            return;
        }

        // can happen if connecting to a non-bitcoin server (like HTTP)
        cb(new Error('disconnect without initial response'));
    });

    peer.once('error', function (err) {
        cb(err);
    });

    peer.connect();
}

Install the necessary packages with:

npm install bitcore-p2p assert-plus request

Conclusion

I learned a lot during this whole process, especially around the value of running my own node to help strengthen the network, even if only by a little. I encourage everyone who has any interest in Bitcoin to run their own full node as well :).