Running a Bitcoin Full Node on SmartOS
Posted by Dave Eddy on Jan 20 2019 - tags: techRunning 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.
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 &' 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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
* 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 :).