Knots RDTS rollback script.
Simple Knots rollback bash script.
Well folks gave up on the bip110 fork pretty quick. Lasted less than 24 hours. I’m going to keep running knots because I like all the extra knobs and switches it gives me to customize my node, but in order to resync to the active chain it’s necessary to roll back to the pre-RDTS enforced version. Here’s a script that will do that:
edit the directory variables at the top to match your local config
#!/usr/bin/env bash
set -Eeuo pipefail
# Tailored to the user's existing Bitcoin Knots user-service installation.
VER='29.3.knots20260507'
UNIT='bitcoind.service'
DATADIR="$HOME/blockchain/bitcoin"
CONF="$DATADIR/bitcoin.conf"
RWCONF="$DATADIR/bitcoin_rw.conf"
DAEMON_LINK="$HOME/bin/bitcoind-knots"
CLI_LINK="$HOME/bin/bitcoin-cli-knots"
INSTALL_PARENT="$HOME/apps/bitcoin-knots"
TARGET_ROOT="$INSTALL_PARENT/bitcoin-$VER"
TARGET_DAEMON="$TARGET_ROOT/bin/bitcoind"
TARGET_CLI="$TARGET_ROOT/bin/bitcoin-cli"
BASE_URL="https://bitcoinknots.org/files/29.x/$VER"
ASSET="bitcoin-$VER-x86_64-linux-gnu.tar.gz"
ASSET_SHA256='383c6ac982972005c3db30af9ac5410f30ef0c139365e50a2db28625160e61d6'
# Fork anchors published in the BIP-110 recovery guide.
COMMON_961631='00000000000000000000807f9dc917442a67910426d79ebb2f8aa2149327ce8a'
DEAD_961632='0000000000000000000169eb6f811ddbd0daf343af7b62180cdb13e7c78dbc16'
LIVE_961632='00000000000000000000d1e01392faa65ceeaed307f0a3159144b84146ff24ba'
WORK="$(mktemp -d)"
CONF_TMP=''
BACKUP=''
UNSAFE_TRANSITION=0
PRE_RDTS_RUNNING=0
INVALIDATED_DEAD_BRANCH=0
cleanup() {
rm -rf -- "$WORK"
[[ -z "$CONF_TMP" ]] || rm -f -- "$CONF_TMP"
}
on_error() {
local status=$? line="${BASH_LINENO[0]:-unknown}"
trap - ERR HUP INT TERM
set +e
echo >&2
if (( UNSAFE_TRANSITION == 1 )); then
systemctl --user stop "$UNIT" >/dev/null 2>&1 || true
echo >&2 "FAIL: recovery stopped near script line $line (status $status)."
if systemctl --user is-active --quiet "$UNIT"; then
echo >&2 'CRITICAL: the Bitcoin service could not be confirmed stopped.'
else
echo >&2 'The Bitcoin service was left stopped rather than returning to RDTS enforcement.'
fi
elif (( PRE_RDTS_RUNNING == 1 )); then
echo >&2 "FAIL: recovery did not complete near script line $line (status $status)."
echo >&2 'The pre-RDTS node was left in its current state for inspection.'
else
echo >&2 "FAIL: preflight failed near script line $line (status $status)."
echo >&2 'The original running node and configuration were not changed.'
fi
[[ -z "$BACKUP" ]] || echo >&2 "Configuration backup: $BACKUP"
cleanup
exit "$status"
}
on_signal() {
local signal="$1" status=130
[[ "$signal" != 'TERM' ]] || status=143
trap - ERR HUP INT TERM
set +e
echo >&2
if (( UNSAFE_TRANSITION == 1 )); then
systemctl --user stop "$UNIT" >/dev/null 2>&1 || true
if systemctl --user is-active --quiet "$UNIT"; then
echo >&2 "Interrupted by $signal; CRITICAL: Bitcoin could not be confirmed stopped."
else
echo >&2 "Interrupted by $signal; Bitcoin was left stopped during the transition."
fi
elif (( PRE_RDTS_RUNNING == 1 )); then
echo >&2 "Interrupted by $signal; the pre-RDTS node remains in its current state."
else
echo >&2 "Interrupted by $signal before the transition; the original node remains unchanged."
fi
cleanup
exit "$status"
}
trap cleanup EXIT
trap on_error ERR
trap 'on_signal HUP' HUP
trap 'on_signal INT' INT
trap 'on_signal TERM' TERM
rpc() {
"$CLI_LINK" -datadir="$DATADIR" -conf="$CONF" "$@"
}
atomic_link() {
local target="$1" link="$2" tmp="${2}.new.$$"
rm -f -- "$tmp"
ln -s -- "$target" "$tmp"
mv -Tf -- "$tmp" "$link"
}
wait_rpc() {
local attempt
for ((attempt=0; attempt<180; attempt++)); do
if systemctl --user is-active --quiet "$UNIT" &&
rpc getnetworkinfo >"$WORK/network.json" 2>/dev/null; then
return 0
fi
sleep 2
done
return 1
}
print_diagnostics() {
set +e
echo
echo '=== DIAGNOSTICS ==='
systemctl --user status "$UNIT" --no-pager -l || true
rpc getblockchaininfo 2>/dev/null | jq '{blocks,headers,bestblockhash,initialblockdownload,verificationprogress}' || true
rpc getchaintips 2>/dev/null | jq '[.[] | {height,hash,branchlen,status}]' || true
rpc getpeerinfo 2>/dev/null | jq '[.[] | {addr,subver,startingheight,synced_headers,synced_blocks}]' || true
journalctl --user -u "$UNIT" -n 120 --no-pager || true
set -e
}
# ----- Preflight -----
(( EUID != 0 )) || {
echo >&2 'FAIL: run this as the normal user that owns bitcoind.service; do not use sudo.'
exit 1
}
[[ "$(uname -m)" == 'x86_64' ]] || {
echo >&2 "FAIL: this script pins the x86_64 release, but uname -m returned $(uname -m)."
exit 1
}
[[ -f "$CONF" ]] || { echo >&2 "FAIL: missing $CONF"; exit 1; }
[[ -r "$CONF" && -w "$CONF" && -w "$DATADIR" ]] || {
echo >&2 "FAIL: $CONF and $DATADIR must be readable/writable by the current user."
exit 1
}
[[ -x "$DAEMON_LINK" ]] || { echo >&2 "FAIL: missing executable $DAEMON_LINK"; exit 1; }
[[ -x "$CLI_LINK" ]] || { echo >&2 "FAIL: missing executable $CLI_LINK"; exit 1; }
[[ -w "$(dirname -- "$DAEMON_LINK")" && -w "$(dirname -- "$CLI_LINK")" ]] || {
echo >&2 'FAIL: the binary symlink directory is not writable by the current user.'
exit 1
}
command -v curl >/dev/null
command -v sha256sum >/dev/null
command -v tar >/dev/null
command -v diff >/dev/null
command -v jq >/dev/null
CONFIG_FILES=("$CONF")
[[ ! -f "$RWCONF" ]] || CONFIG_FILES+=("$RWCONF")
if grep -HEn '^[[:space:]]*includeconf[[:space:]]*=' "${CONFIG_FILES[@]}"; then
echo >&2 'FAIL: includeconf is present. Inspect every included file before changing consensus software.'
exit 1
fi
if [[ -f "$RWCONF" ]] && grep -HEn '^[[:space:]]*consensusrules[[:space:]]*=' "$RWCONF"; then
echo >&2 'FAIL: bitcoin_rw.conf contains consensusrules. It requires manual inspection.'
exit 1
fi
bad_rules="$(awk '
/^[[:space:]]*consensusrules[[:space:]]*=/ {
value=$0
sub(/^[^=]*=/, "", value)
sub(/[[:space:]]*#.*/, "", value)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
if (value != "rdts") print NR ":" $0
}
' "$CONF")"
if [[ -n "$bad_rules" ]]; then
echo >&2 'FAIL: unexpected consensusrules setting:'
printf '%s\n' "$bad_rules" >&2
exit 1
fi
systemctl --user is-active --quiet "$UNIT" || {
echo >&2 "FAIL: $UNIT is not active. Inspect its state before running this transition."
exit 1
}
OLD_DAEMON="$(readlink -f -- "$DAEMON_LINK")"
OLD_CLI="$(readlink -f -- "$CLI_LINK")"
[[ -x "$OLD_DAEMON" && -x "$OLD_CLI" ]]
CURRENT_PID="$(systemctl --user show "$UNIT" -p MainPID --value)"
CURRENT_EXE="$(readlink -f "/proc/$CURRENT_PID/exe")"
[[ "$CURRENT_EXE" == "$OLD_DAEMON" ]] || {
echo >&2 "FAIL: running executable $CURRENT_EXE does not match $DAEMON_LINK -> $OLD_DAEMON."
exit 1
}
rpc getnetworkinfo >"$WORK/network-before.json"
rpc getblockchaininfo >"$WORK/chain-before.json"
BEFORE_HEIGHT="$(jq -er '.blocks' "$WORK/chain-before.json")"
BEFORE_961632='below-height'
if (( BEFORE_HEIGHT >= 961632 )); then
BEFORE_961632="$(rpc getblockhash 961632)"
fi
echo '=== CURRENT NODE ==='
printf 'Daemon: %s\n' "$OLD_DAEMON"
"$OLD_DAEMON" --version | sed -n '1p'
jq '{version,subversion,connections,localservicesnames}' "$WORK/network-before.json"
jq '{blocks,headers,bestblockhash,initialblockdownload,verificationprogress}' "$WORK/chain-before.json"
printf 'Block 961632: %s\n' "$BEFORE_961632"
# ----- Install the matching pre-RDTS release without touching the datadir -----
# Always fetch and verify the pinned archive before trusting any same-named local
# installation. An existing target directory must match the verified archive.
echo
echo '=== DOWNLOAD VERIFIED PRE-RDTS KNOTS ==='
mkdir -p -- "$INSTALL_PARENT"
cd -- "$WORK"
curl --fail --location --proto '=https' --tlsv1.2 \
--output "$ASSET" "$BASE_URL/$ASSET"
printf '%s %s\n' "$ASSET_SHA256" "$ASSET" | sha256sum -c -
tar -xzf "$ASSET" -C "$WORK"
EXTRACTED_ROOT="$WORK/bitcoin-$VER"
[[ -x "$EXTRACTED_ROOT/bin/bitcoind" && -x "$EXTRACTED_ROOT/bin/bitcoin-cli" ]]
[[ "$("$EXTRACTED_ROOT/bin/bitcoind" --version | sed -n '1p')" == "Bitcoin Knots daemon version v$VER" ]]
[[ "$("$EXTRACTED_ROOT/bin/bitcoin-cli" --version | sed -n '1p')" == "Bitcoin Knots RPC client version v$VER" ]]
if [[ -e "$TARGET_ROOT" || -L "$TARGET_ROOT" ]]; then
[[ -d "$TARGET_ROOT" && ! -L "$TARGET_ROOT" ]] || {
echo >&2 "FAIL: $TARGET_ROOT exists but is not a normal directory."
exit 1
}
if ! diff -qr --no-dereference "$EXTRACTED_ROOT" "$TARGET_ROOT" >"$WORK/target-diff.txt"; then
echo >&2 "FAIL: existing $TARGET_ROOT does not match the verified release archive."
sed -n '1,80p' "$WORK/target-diff.txt" >&2
exit 1
fi
rm -rf -- "$EXTRACTED_ROOT"
else
mv -- "$EXTRACTED_ROOT" "$TARGET_ROOT"
fi
DAEMON_BANNER="$("$TARGET_DAEMON" --version | sed -n '1p')"
CLI_BANNER="$("$TARGET_CLI" --version | sed -n '1p')"
[[ "$DAEMON_BANNER" == "Bitcoin Knots daemon version v$VER" ]]
[[ "$CLI_BANNER" == "Bitcoin Knots RPC client version v$VER" ]]
echo
printf 'Target daemon: %s\n' "$TARGET_DAEMON"
printf '%s\n' "$DAEMON_BANNER"
# ----- Clean transition to non-enforcing software -----
BACKUP="$CONF.pre-rdts-recovery-$(date -u +%Y%m%dT%H%M%SZ)"
cp -a -- "$CONF" "$BACKUP"
printf 'Configuration backup: %s\n' "$BACKUP"
echo
echo '=== STOP NODE AND REMOVE RDTS CONSENT ==='
UNSAFE_TRANSITION=1
systemctl --user stop "$UNIT"
if systemctl --user is-active --quiet "$UNIT"; then
echo >&2 'FAIL: service remained active after stop.'
exit 1
fi
CONF_TMP="$(mktemp "$DATADIR/.bitcoin.conf.pre-rdts.XXXXXXXX")"
awk '
/^[[:space:]]*#[[:space:]]*Explicit consent to enforce BIP110\/RDTS consensus rules[[:space:]]*$/ { next }
/^[[:space:]]*consensusrules[[:space:]]*=[[:space:]]*rdts([[:space:]]*#.*)?[[:space:]]*$/ { next }
{ print }
' "$CONF" >"$CONF_TMP"
chmod --reference="$CONF" "$CONF_TMP"
mv -f -- "$CONF_TMP" "$CONF"
CONF_TMP=''
if grep -HEn '^[[:space:]]*consensusrules[[:space:]]*=' "${CONFIG_FILES[@]}"; then
echo >&2 'FAIL: a consensusrules setting remains after editing.'
false
fi
atomic_link "$TARGET_DAEMON" "$DAEMON_LINK"
atomic_link "$TARGET_CLI" "$CLI_LINK"
systemctl --user reset-failed "$UNIT" >/dev/null 2>&1 || true
systemctl --user start "$UNIT"
wait_rpc
PID="$(systemctl --user show "$UNIT" -p MainPID --value)"
ACTUAL_EXE="$(readlink -f "/proc/$PID/exe")"
[[ "$ACTUAL_EXE" == "$TARGET_DAEMON" ]]
rpc getnetworkinfo >"$WORK/network-after-switch.json"
rpc getdeploymentinfo >"$WORK/deployment-after-switch.json"
jq -e '(.deployments // {} | has("reduced_data")) | not' "$WORK/deployment-after-switch.json" >/dev/null
PRE_RDTS_RUNNING=1
echo
echo '=== PRE-RDTS NODE RUNNING ==='
printf 'Executable: %s\n' "$ACTUAL_EXE"
rpc getnetworkinfo | jq '{version,subversion,connections,localservicesnames}'
# ----- Leave the stopped BIP-110 branch -----
CURRENT_HEIGHT="$(rpc getblockcount)"
CURRENT_961632='below-height'
if (( CURRENT_HEIGHT >= 961632 )); then
CURRENT_961632="$(rpc getblockhash 961632)"
fi
case "$CURRENT_961632" in
"$DEAD_961632")
echo
echo '=== LEAVE STOPPED BIP-110 BRANCH ==='
rpc invalidateblock "$DEAD_961632"
INVALIDATED_DEAD_BRANCH=1
echo "Invalidated stopped-branch root: $DEAD_961632"
;;
"$LIVE_961632")
echo 'Node is already on the surviving Bitcoin branch at height 961632.'
;;
'below-height')
echo 'Node is below height 961632 and will validate forward under pre-RDTS rules.'
;;
*)
echo >&2 "FAIL: unexpected block hash at height 961632: $CURRENT_961632"
false
;;
esac
# From this point onward, leaving the verified pre-RDTS node running is safer
# than automatically restoring the RDTS-enforcing binary.
UNSAFE_TRANSITION=0
# ----- Wait for the surviving branch and current tip -----
echo
echo '=== SYNC SURVIVING BITCOIN BRANCH ==='
SYNCED=0
for ((attempt=0; attempt<720; attempt++)); do
if CHAIN_JSON="$(rpc getblockchaininfo 2>/dev/null)"; then
BLOCKS="$(jq -er '.blocks' <<<"$CHAIN_JSON")"
HEADERS="$(jq -er '.headers' <<<"$CHAIN_JSON")"
IBD="$(jq -r '.initialblockdownload' <<<"$CHAIN_JSON")"
ACTIVE_961632='below-height'
if (( BLOCKS >= 961632 )); then
ACTIVE_961632="$(rpc getblockhash 961632 2>/dev/null || true)"
fi
if (( attempt % 12 == 0 )); then
printf 'blocks=%s headers=%s ibd=%s block961632=%s\n' \
"$BLOCKS" "$HEADERS" "$IBD" "$ACTIVE_961632"
fi
if [[ "$ACTIVE_961632" == "$LIVE_961632" && "$BLOCKS" == "$HEADERS" && "$IBD" == 'false' ]]; then
SYNCED=1
break
fi
fi
sleep 5
done
if (( SYNCED != 1 )); then
echo >&2 'FAIL: the node did not reach the verified surviving-chain state.'
print_diagnostics
exit 2
fi
# Clear only the temporary local invalid mark used to force the initial branch exit.
# The surviving chain remains active because it has more accumulated proof of work.
if (( INVALIDATED_DEAD_BRANCH == 1 )); then
echo
echo '=== CLEAR TEMPORARY LOCAL INVALIDATION ==='
rpc reconsiderblock "$DEAD_961632"
fi
sleep 2
FINAL_CHAIN="$(rpc getblockchaininfo)"
FINAL_DEPLOYMENT="$(rpc getdeploymentinfo)"
FINAL_961631="$(rpc getblockhash 961631)"
FINAL_961632="$(rpc getblockhash 961632)"
[[ "$FINAL_961631" == "$COMMON_961631" ]]
[[ "$FINAL_961632" == "$LIVE_961632" ]]
jq -e '.blocks == .headers and .initialblockdownload == false' <<<"$FINAL_CHAIN" >/dev/null
jq -e '(.deployments // {} | has("reduced_data")) | not' <<<"$FINAL_DEPLOYMENT" >/dev/null
echo
echo '=== VERIFIED RECOVERY ==='
printf 'Executable: %s\n' "$(readlink -f "/proc/$(systemctl --user show "$UNIT" -p MainPID --value)/exe")"
printf 'Block 961631: %s\n' "$FINAL_961631"
printf 'Block 961632: %s\n' "$FINAL_961632"
jq '{blocks,headers,bestblockhash,initialblockdownload,verificationprogress}' <<<"$FINAL_CHAIN"
rpc getchaintips | jq '[.[] | {height,hash,branchlen,status}]'
echo
echo 'PASS: RDTS enforcement is disabled and the node is fully synced to the surviving Bitcoin chain.'
echo 'Keep dependent services stopped until each one separately reports fully synced.'
Write a comment