Skip to content

Instantly share code, notes, and snippets.

@wileyj
Last active November 9, 2023 22:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wileyj/8502b7ed50117f5ccdbb248502f0fe77 to your computer and use it in GitHub Desktop.
Save wileyj/8502b7ed50117f5ccdbb248502f0fe77 to your computer and use it in GitHub Desktop.
Stackerdb script
#!/usr/bin/env bash
set -eo pipefail
set -Eo functrace
shopt -s expand_aliases
##
## Meant to be run manually or via cron
## ex cron:
## 00 * * * * /usr/local/bin/stackerdb >> /tmp/stackerdb-log
##
## set some defaults
## Optional: use verbose logging
VERBOSE=true
INIT=false
## Optional: use an auth user/key to bypass API rate limits in the format of "-u user:key"
AUTH=""
## Local file paths
LOCAL_VERSION="/stackerdb_sha"
LOCAL_KEYFILE="/stacks_key"
LOCAL_BINARY_DIR="/usr/local/bin"
LOCAL_BINARY_NAME="stacks-node"
LOCAL_BINARY="${LOCAL_BINARY_DIR}/${LOCAL_BINARY_NAME}"
LOCAL_KEY_SCRIPT="${LOCAL_BINARY_DIR}/key"
## stacks specific values
STACKS_SERVICE_USER="stacks"
STACKS_SERVICE_NAME="stacks"
STACKS_CONFIG_DIR="/etc/stacks-blockchain"
STACKS_CHAINSTATE_DIR="/stacks-blockchain"
STACKS_CONFIG_FILE="${STACKS_CONFIG_DIR}/Config.toml"
STACKS_UNIT_FILE="/etc/systemd/system/${STACKS_SERVICE_NAME}.service"
STACKS_RPC_PORT=20443
STACKS_P2P_PORT=20444
GIT_DIR="${HOME}/stacks-blockchain"
REMOTE_BRANCH="develop"
## Optional but recommended for keeping the keypair consistent (can be retrieved after 1st run from /stacks_key)
STACKS_PRIVATE_KEY=""
STACKS_PUBLIC_KEY=""
BINARY_LIST=(
blockstack-cli
clarity-cli
stacks-events
stacks-signer
stacks-node
stacks-inspect
)
## configure logging format
alias log="logger"
alias log_error='logger "${ERROR}"'
alias log_warn='logger "${WARN}"'
alias log_info='logger "${INFO}"'
alias log_exit='exit_error "${EXIT_MSG}"'
if ${VERBOSE}; then
alias log='logger "$(date "+%D %H:%m:%S")" "Func:${FUNCNAME:-main}" "Line:${LINENO:-null}"'
alias log_info='logger "$(date "+%D %H:%m:%S")" "Func:${FUNCNAME:-main}" "Line:${LINENO:-null}" "${INFO}"'
alias log_warn='logger "$(date "+%D %H:%m:%S")" "Func:${FUNCNAME:-main}" "Line:${LINENO:-null}" "${WARN}"'
alias log_error='logger "$(date "+%D %H:%m:%S")" "Func:${FUNCNAME:-main}" "Line:${LINENO:-null}" "${ERROR}"'
alias log_exit='exit_error "$(date "+%D %H:%m:%S")" "Func:${FUNCNAME:-main}" "Line:${LINENO:-null}" "${EXIT_MSG}"'
fi
logger() {
if ${VERBOSE}; then
printf "%s %-30s %-10s %-10s %-25s %s\\n" "${1}" "${2}" "${3}" "${DEBUG}" "${4}" "${5}"
else
printf "%-25s %s\\n" "${1}" "${2}"
fi
}
exit_error() {
if ${VERBOSE}; then
printf "%s %-25s %-10s %-10s %-25s %s\\n\\n" "${1}" "${2}" "${DEBUG}" "${3}" "${4}" "${5}"
else
printf "%-25s %s\\n\\n" "${1}" "${2}"
fi
exit 1
}
if ! (sudo test -f "${LOCAL_VERSION}" && sudo test -f "${LOCAL_KEYFILE}" ); then
INIT=true
${VERBOSE} && log "Local state files are missing. Trying to install dependencies"
log "Installing system dependencies"
## install required packages
sudo apt-get update && sudo apt-get install -y curl clang nodejs npm jq sed > /dev/null 2>&1 || log_exit "Error installing required packages"
## install required npm packages
sudo npm install -g @stacks/encryption elliptic > /dev/null 2>&1 || log_exit "Error installing required npm modules"
## clone stacks-blockchain repo
if [ -d "${GIT_DIR}" ]; then
rm -rf "${GIT_DIR}"
fi
git clone https://github.com/stacks-network/stacks-blockchain --branch ${REMOTE_BRANCH} --single-branch ${GIT_DIR} > /dev/null 2>&1 || log_exit "Error cloning stacks-blockchain repo"
## install rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y > /dev/null 2>&1 || log_exit "Error installing rust"
fi
## update $PATH for rest of the script
source "$HOME/.cargo/env"
# Check for required binaries, exit if missing
for cmd in curl node cargo sed cargo; do
command -v "${cmd}" > /dev/null 2>&1 || log_exit "Missing command: ${cmd}"
done
## Only set these values after dependencies are installed and we've checked the binaries are in $PATH
NODE_PATH="$(npm root -g)"
## if verbose is set, print out the variables we'll be using throughout the script
${VERBOSE} && log "NODE_PATH: ${NODE_PATH}"
${VERBOSE} && log "VERBOSE: ${VERBOSE}"
${VERBOSE} && log "INIT: ${INIT}"
${VERBOSE} && log "LOCAL_VERSION: ${LOCAL_VERSION}"
${VERBOSE} && log "LOCAL_KEYFILE: ${LOCAL_KEYFILE}"
${VERBOSE} && log "LOCAL_BINARY_DIR: ${LOCAL_BINARY_DIR}"
${VERBOSE} && log "LOCAL_BINARY_NAME: ${LOCAL_BINARY_NAME}"
${VERBOSE} && log "LOCAL_BINARY: ${LOCAL_BINARY}"
${VERBOSE} && log "LOCAL_KEY_SCRIPT: ${LOCAL_KEY_SCRIPT}"
${VERBOSE} && log "STACKS_SERVICE_USER: ${STACKS_SERVICE_USER}"
${VERBOSE} && log "STACKS_SERVICE_NAME: ${STACKS_SERVICE_NAME}"
${VERBOSE} && log "STACKS_CONFIG_DIR: ${STACKS_CONFIG_DIR}"
${VERBOSE} && log "STACKS_CHAINSTATE_DIR: ${STACKS_CHAINSTATE_DIR}"
${VERBOSE} && log "STACKS_CONFIG_FILE: ${STACKS_CONFIG_FILE}"
${VERBOSE} && log "STACKS_UNIT_FILE: ${STACKS_UNIT_FILE}"
${VERBOSE} && log "STACKS_RPC_PORT: ${STACKS_RPC_PORT}"
${VERBOSE} && log "STACKS_P2P_PORT: ${STACKS_P2P_PORT}"
${VERBOSE} && log "GIT_DIR: ${GIT_DIR}"
${VERBOSE} && log "REMOTE_BRANCH: ${REMOTE_BRANCH}"
## Install a simple node script to create a stacks keypair
install_seed_script() {
## create/recreate the script every runtime
log "Creating file: ${LOCAL_KEY_SCRIPT}"
sudo bash -c 'cat <<-EOF> '"${LOCAL_KEY_SCRIPT}"'
#!/usr/bin/env node
const EC = require("elliptic").ec;
const secp256k1 = new EC("secp256k1");
const enc = require("@stacks/encryption");
const process = require("process");
const key = secp256k1.genKeyPair();
while (true) {
const key_hex = key.getPrivate().toString("hex");
if (key_hex.length != 64) {
continue;
}
break;
}
try {
const privateKey = key.getPrivate().toString("hex");
const publicKey = enc.getPublicKeyFromPrivate(privateKey);
process.stdout.write([publicKey, ":", privateKey].join(""));
} catch {
return process.exit(1);
}
return process.exit(0);
EOF'
## set the script as executable
${VERBOSE} && log "Setting file permissions on ${LOCAL_KEY_SCRIPT}"
sudo chmod 755 "${LOCAL_KEY_SCRIPT}" || log_exit "Error creating script: ${LOCAL_KEY_SCRIPT}"
return 0
}
## Install the systemd unit for stacks
install_unit_file() {
## create/recreate the unit file every runtime
log "Creating unit file: ${STACKS_UNIT_FILE}"
sudo bash -c 'cat <<-EOF> '"${STACKS_UNIT_FILE}"'
## Modeled after https://github.com/bitcoin/bitcoin/blob/master/contrib/init/bitcoind.service
[Unit]
Description=Stacks Blockchain
# https://www.freedesktop.org/wiki/Software/systemd/NetworkTarget/
After=network-online.target
Wants=network-online.target
ConditionFileIsExecutable=LOCAL_BINARY
ConditionPathExists=STACKS_CONFIG_FILE
ConditionPathIsDirectory=STACKS_CHAINSTATE_DIR
[Service]
ExecStart=LOCAL_BINARY start --config=STACKS_CONFIG_FILE
# Make sure the config directory is readable by the service user
PermissionsStartOnly=true
ExecStartPre=/bin/chgrp stacks STACKS_CONFIG_DIR
# Process management
####################
PIDFile=/run/stacks-blockchain/stacks-blockchain.pid
Restart=no
TimeoutStopSec=900
KillSignal=SIGINT
SendSIGKILL=no
# Directory creation and permissions
####################################
# Run as SERVICE_USER:SERVICE_USER
User=STACKS_SERVICE_USER
Group=STACKS_SERVICE_USER
# /run/stacks-blockchain
RuntimeDirectory=stacks-blockchain
RuntimeDirectoryMode=0710
# /etc/stacks-blockchain
ConfigurationDirectory=stacks-blockchain
ConfigurationDirectoryMode=0710
# Hardening measures
####################
# Provide a private /tmp and /var/tmp.
PrivateTmp=true
# Mount /usr, /boot/ and /etc read-only for the process.
ProtectSystem=full
# Deny access to /home, /root and /run/user
ProtectHome=true
# Disallow the process and all of its children to gain
# new privileges through execve().
NoNewPrivileges=true
# Use a new /dev namespace only populated with API pseudo devices
# such as /dev/null, /dev/zero and /dev/random.
PrivateDevices=true
# Deny the creation of writable and executable memory mappings.
MemoryDenyWriteExecute=true
SystemCallArchitectures=native
[Install]
WantedBy=multi-user.target
EOF'
if ! sudo test -f "${STACKS_UNIT_FILE}";then
log_exit "Error creating unit file: ${STACKS_UNIT_FILE}"
fi
## replace some hardcoded values in the above file with variables from this script
${VERBOSE} && log "Substitute unit file strings with variables"
sudo sed -i -e "s|STACKS_SERVICE_USER|${STACKS_SERVICE_USER}|g" "${STACKS_UNIT_FILE}" || log_exit "Error sed'ing 'SERVICE_USER' in ${STACKS_UNIT_FILE}"
sudo sed -i -e "s|STACKS_CONFIG_DIR|${STACKS_CONFIG_DIR}|g" "${STACKS_UNIT_FILE}" || log_exit "Error sed'ing 'STACKS_CONFIG_DIR' in ${STACKS_UNIT_FILE}"
sudo sed -i -e "s|STACKS_CHAINSTATE_DIR|${STACKS_CHAINSTATE_DIR}|g" "${STACKS_UNIT_FILE}" || log_exit "Error sed'ing 'STACKS_CHAINSTATE_DIR' in ${STACKS_UNIT_FILE}"
sudo sed -i -e "s|STACKS_CONFIG_FILE|${STACKS_CONFIG_FILE}|g" "${STACKS_UNIT_FILE}" || log_exit "Error sed'ing 'STACKS_CONFIG_FILE' in ${STACKS_UNIT_FILE}"
sudo sed -i -e "s|LOCAL_BINARY|${LOCAL_BINARY}|g" "${STACKS_UNIT_FILE}" || log_exit "Error sed'ing 'LOCAL_BINARY' in ${STACKS_UNIT_FILE}"
## set the unit's file permissions
${VERBOSE} && log "Setting file permissions on ${STACKS_UNIT_FILE}"
sudo chmod 644 "${STACKS_UNIT_FILE}" || log_exit "Error setting permissions on ${STACKS_UNIT_FILE}"
## reload systemd and enable the unit
${VERBOSE} && log "Reloading systemd"
sudo systemctl daemon-reload > /dev/null 2>&1 || log_exit "Error reloading systemd"
${VERBOSE} && log "Enabling ${STACKS_SERVICE_NAME} on boot"
sudo systemctl enable ${STACKS_SERVICE_NAME} > /dev/null 2>&1 || log_exit "Error enabling ${STACKS_SERVICE_NAME} on boot"
log "Installed and enabled systemd unit"
return 0
}
## Install the stacks Config.toml
install_stacks_config() {
private_key=""
public_key=""
local_public_ipv4=""
## check for an existing keyfile to use values from
if ! sudo test -f "${LOCAL_KEYFILE}"; then
if [ "${STACKS_PRIVATE_KEY}" != "" -a "${STACKS_PUBLIC_KEY}" != "" ];then
## use hard-coded values if they are defined and local keyfile is missing
log "Creating keyfile from hardcoded stacks keypair"
sudo sh -c "echo -n ${STACKS_PUBLIC_KEY}:${STACKS_PRIVATE_KEY} > ${LOCAL_KEYFILE}" || log_exit "Error storing version in: ${LOCAL_KEYFILE}"
else
## create a new keyfile with new keypair from script in install_seed_script
log "Creating new stacks keypair"
sudo sh -c "NODE_PATH=${NODE_PATH} node ${LOCAL_KEY_SCRIPT} > ${LOCAL_KEYFILE}" || log_exit "Error saving key to: ${LOCAL_KEYFILE}"
fi
fi
## ensure the keyfile has the correct permissions
sudo chmod 600 ${LOCAL_KEYFILE} || log_exit "Error setting permissions: ${LOCAL_KEYFILE}"
## retrieve the keys from the (now) existing file
public_key=$(sudo cat ${LOCAL_KEYFILE} | cut -f1 -d ":")
private_key=$(sudo cat ${LOCAL_KEYFILE} | cut -f2 -d ":")
if [ "${private_key}" == "" ]; then
log_exit "Error retrieving private key from: ${LOCAL_KEYFILE}"
fi
${VERBOSE} && log "using private_key: ${private_key}"
${VERBOSE} && log "using public_key: ${public_key}"
## if the config file does not exist or the public ip has changed, (over)write the config
if ! (sudo test -f "${STACKS_CONFIG_FILE}" ); then
log "Creating config file: ${STACKS_CONFIG_FILE}"
sudo bash -c 'cat <<-EOF> '"${STACKS_CONFIG_FILE}"'
[node]
working_dir = "STACKS_CHAINSTATE_DIR"
rpc_bind = "0.0.0.0:STACKS_RPC_PORT"
p2p_bind = "0.0.0.0:STACKS_P2P_PORT"
local_peer_seed = "PRIVATE_KEY"
bootstrap_node = "02196f005965cebe6ddc3901b7b1cc1aa7a88f305bb8c5893456b8f9a605923893@seed.mainnet.hiro.so:20444,02539449ad94e6e6392d8c1deb2b4e61f80ae2a18964349bc14336d8b903c46a8c@cet.stacksnodes.org:20444,02ececc8ce79b8adf813f13a0255f8ae58d4357309ba0cedd523d9f1a306fcfb79@sgt.stacksnodes.org:20444,0303144ba518fe7a0fb56a8a7d488f950307a4330f146e1e1458fc63fb33defe96@est.stacksnodes.org:20444"
name = "stackerdb"
[burnchain]
chain = "bitcoin"
mode = "mainnet"
peer_host = "bitcoin.mainnet.stacks.org"
username = "stacks"
password = "foundation"
rpc_port = 8332
peer_port = 8333
EOF'
## replace some hardcoded values in the above file with variables from this script
${VERBOSE} && log "Substitute config file strings with variables"
sudo sed -i -e "s|STACKS_RPC_PORT|${STACKS_RPC_PORT}|g" "${STACKS_CONFIG_FILE}" || log_exit "Error sed'ing 'RPC_PORT' in ${STACKS_CONFIG_FILE}"
sudo sed -i -e "s|STACKS_P2P_PORT|${STACKS_P2P_PORT}|g" "${STACKS_CONFIG_FILE}" || log_exit "Error sed'ing 'P2P_PORT' in ${STACKS_CONFIG_FILE}"
sudo sed -i -e "s|STACKS_CHAINSTATE_DIR|${STACKS_CHAINSTATE_DIR}|g" "${STACKS_CONFIG_FILE}" || log_exit "Error sed'ing 'STACKS_CHAINSTATE_DIR' in ${STACKS_CONFIG_FILE}"
sudo sed -i -e "s|PRIVATE_KEY|${private_key}|g" "${STACKS_CONFIG_FILE}" || log_exit "Error sed'ing 'PRIVATE_KEY' in ${STACKS_CONFIG_FILE}"
if ! sudo test -f "${STACKS_CONFIG_FILE}";then
log_exit "Error creating config file: ${STACKS_CONFIG_FILE}"
fi
if is_stacks_running; then
${VERBOSE} && log "Stopping ${STACKS_SERVICE_NAME} after config change"
## The stacks binary is running, stop it. the last step in the script will restart the pid, no need to do it here
run_systemctl "stop"
fi
fi
## change the group to the service user (will need to have read access to this file when binary is started)
sudo chgrp "${STACKS_SERVICE_USER}" "${STACKS_CONFIG_FILE}" || log_exit "Error setting group on: ${STACKS_CONFIG_FILE}"
return 0
}
## Create the user to own the pid and files/dirs
create_user(){
## check if the user exists
if [ ! $(getent passwd ${STACKS_SERVICE_USER}) ]; then
## create the service user with a bash shell using the chainstate dir as $HOME
sudo useradd "${STACKS_SERVICE_USER}" -s /usr/bin/bash -m -d "${STACKS_CHAINSTATE_DIR}" || log_exit "Error creating user: ${STACKS_SERVICE_USER}"
log "Created user: ${STACKS_SERVICE_USER}"
else
${VERBOSE} && log "User already exists: ${STACKS_SERVICE_USER}"
fi
return 0
}
## create the expected dirs if they don't exist
create_dirs() {
dirs=(
${STACKS_CONFIG_DIR}
${STACKS_CHAINSTATE_DIR}
)
for dir in "${dirs[@]}"; do
## if the dir is missing, create it
if ! sudo test -d "${dir}";then
log "Creating missing dir: $dir"
sudo mkdir -p "${dir}" || log_exit "Error creating dir: ${dir}"
## set the ownership only if we're createing the dir
sudo chown -R "${STACKS_SERVICE_USER}" "${dir}" || log_exit "Error setting ownership on: ${dir}"
fi
## Set permissions on the dirs (regardless if they were just created)
log "Setting permissions on: ${dir}"
if [ "${dir}" == "${STACKS_CONFIG_DIR}" ];then
${VERBOSE} && log "Setting permissions on: ${dir}"
sudo chmod 710 "${dir}" || log_exit "Error setting permissions on: ${dir}"
fi
## set the group ownership of the dir (less relevant for the chainstate, but important for the config dir/file)
log "Setting ownership on: ${dir}"
sudo chgrp "${STACKS_SERVICE_USER}" "${dir}" || log_exit "Error setting group on: ${dir}"
done
return 0
}
## Check if the binary is running via systemd
is_stacks_running() {
sudo systemctl status "${STACKS_SERVICE_NAME}" > /dev/null 2>&1
if [[ "${?}" -eq 0 ]]; then
## service is running
return 0
fi
${VERBOSE} && log "${STACKS_SERVICE_NAME} service is not running"
return 1
}
## Execute systemcl command
run_systemctl(){
cmd="${1}"
${VERBOSE} && log "Running: systemctl sudo systemctl ${cmd} ${STACKS_SERVICE_NAME}"
sudo systemctl "${cmd}" "${STACKS_SERVICE_NAME}" > /dev/null 2>&1 || log_exit "Error Running: sudo systemctl ${cmd} ${STACKS_SERVICE_NAME}"
return 0
}
## Compare the remote version to a recorded local version file
check_version() {
local_commit_sha=$(git rev-parse HEAD)
remote_commit_sha=$(git ls-remote https://github.com/stacks-network/stacks-blockchain ${REMOTE_BRANCH} | awk '{print $1}')
## if this is the first run, or if git diff detects a difference then continue
if ${INIT} || [ "${local_commit_sha}" != "${remote_commit_sha}" ];then
# if ${INIT} || ! git diff --exit-code develop origin/${REMOTE_BRANCH} > /dev/null; then
log "Differences found, INIT=${INIT}"
return 0
fi
log "No changes detected in remote"
return 1
}
pull_repo() {
if ! git fetch origin > /dev/null 2>&1; then
log_exit "Error fetching remote origin"
fi
if ! git checkout ${REMOTE_BRANCH} > /dev/null 2>&1; then
log_exit "Error Checking out branch: ${REMOTE_BRANCH}"
fi
if ! git pull -r > /dev/null 2>&1; then
log_exit "Error pulling remote changes"
fi
sudo sh -c "git rev-parse HEAD > ${LOCAL_VERSION}" || log_exit "Error saving commit sha to: ${LOCAL_VERSION}"
return 0
}
build_binary() {
if [ ! -d "${GIT_DIR}/testnet/stacks-node" ]; then
log_exit "Missing dir: ${GIT_DIR}/testnet/stacks-node"
fi
log "Building stacks-node from source"
if ! cargo build --features monitoring_prom,slog_json --release; then
log_exit "Error building stacks-node"
fi
if ! sudo cp -a ${GIT_DIR}/target/release/${LOCAL_BINARY_NAME} ${LOCAL_BINARY}; then
log_exit "Error copying ${LOCAL_BINARY_NAME} to ${LOCAL_BINARY}"
fi
for binary in ${BINARY_LIST[@]}; do
if ! sudo cp -a ${GIT_DIR}/target/release/$binary ${LOCAL_BINARY_DIR}/${binary}; then
log_exit "Error copying ${binary} to ${LOCAL_BINARY_DIR}/${binary}"
fi
done
return 0
}
if [ ! -d "${GIT_DIR}" ];then
log_exit "Missing dir: ${GIT_DIR}"
fi
cd ${GIT_DIR}
# ## Run these functions every time (only `create_user` and `create_dirs` has to be run in order)
create_user ## create the user binary will run as
create_dirs ## create the required dirs
install_seed_script ## install the script to generate a public key
install_unit_file ## install the systemd unit
install_stacks_config ## create the stacks config
## check if the remote sha matches the local version file
if check_version; then
## if there is no match, pull the repo
if pull_repo; then
## check if binary is running
if is_stacks_running; then
## The stacks binary is running, stop it
run_systemctl "stop"
fi
## Extract the stacks-node binary file
if build_binary && ! is_stacks_running; then
## Start the binary
run_systemctl "start"
fi
fi
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment