From ecc400919e4b718bc668eac4cd0b9933e68d3af0 Mon Sep 17 00:00:00 2001 From: lesterli Date: Mon, 21 Oct 2024 00:10:41 +0800 Subject: [PATCH] feat: launch bitcoin node (#3) --- .env.bitcoin.example | 13 +++++ .github/workflows/publish-bitcoin.yml | 69 ++++++++++++++++++++++++++ Makefile | 15 +++++- README.md | 49 +++++++++++++++--- docker/docker-compose-bitcoin.yml | 22 ++++++++ scripts/bitcoin/Dockerfile | 22 ++++++++ scripts/bitcoin/entrypoint.sh | 50 +++++++++++++++++++ scripts/bitcoin/setup-wallet.sh | 44 ++++++++++++++++ scripts/bitcoin/start.sh | 35 +++++++++++++ scripts/bitcoin/stop.sh | 8 +++ scripts/bitcoin/verify-sync-balance.sh | 56 +++++++++++++++++++++ 11 files changed, 375 insertions(+), 8 deletions(-) create mode 100644 .env.bitcoin.example create mode 100644 .github/workflows/publish-bitcoin.yml create mode 100644 docker/docker-compose-bitcoin.yml create mode 100644 scripts/bitcoin/Dockerfile create mode 100755 scripts/bitcoin/entrypoint.sh create mode 100755 scripts/bitcoin/setup-wallet.sh create mode 100755 scripts/bitcoin/start.sh create mode 100755 scripts/bitcoin/stop.sh create mode 100755 scripts/bitcoin/verify-sync-balance.sh diff --git a/.env.bitcoin.example b/.env.bitcoin.example new file mode 100644 index 0000000..e3c07a3 --- /dev/null +++ b/.env.bitcoin.example @@ -0,0 +1,13 @@ +NETWORK=regtest # regtest | signet +RPC_PORT=18443 # 18443 (regtest) | 38332 (signet) +RPC_USER=rpcuser +RPC_PASS=rpcpass +BTC_WALLET_NAME=btcwallet +BTC_WALLET_PASS=walletpass +# btc private key in WIF format +BTC_PRIVKEY=cRvCqiuhGEPeCpkcwttMbNSMDkkaEXmMxSmLWJjPPXzgB3CjdcaS +ZMQ_SEQUENCE_PORT=29000 +ZMQ_RAWBLOCK_PORT=29001 +ZMQ_RAWTR_PORT=29002 +# only used if BITCOIN_NETWORK=regtest +GENERATE_INTERVAL_SECS=600 # 10 minutes \ No newline at end of file diff --git a/.github/workflows/publish-bitcoin.yml b/.github/workflows/publish-bitcoin.yml new file mode 100644 index 0000000..2a5d22d --- /dev/null +++ b/.github/workflows/publish-bitcoin.yml @@ -0,0 +1,69 @@ +name: publish-bitcoin + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + push: + paths: + - 'scripts/bitcoin/**' + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: docker.io + # Remove the slash at the end of the username + IMAGE_NAME: ${{ secrets.DOCKER_HUB_USERNAME }}/bitcoind + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up QEMU for multi-architecture support + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Login against a Docker registry + # https://github.com/docker/login-action + - name: Log into Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Set Docker image tag + id: set-tag + run: | + echo "TAG=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6 + with: + push: true + context: scripts/bitcoin + platforms: linux/amd64,linux/arm64 + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.set-tag.outputs.TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/Makefile b/Makefile index 36748f8..28178a2 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,16 @@ start-babylon: @./scripts/babylon-devnet/init-testnets-dir.sh - @$(DOCKER) compose -f docker/docker-compose-babylon.yml up -d \ No newline at end of file + @docker compose -f docker/docker-compose-babylon.yml up -d +.PHONY: start-babylon + +start-bitcoin: + @./scripts/bitcoin/start.sh +.PHONY: start-bitcoin + +stop-bitcoin: + @./scripts/bitcoin/stop.sh +.PHONY: stop-bitcoin + +verify-bitcoin-sync-balance: + @./scripts/bitcoin/verify-sync-balance.sh +.PHONY: verify-bitcoin-sync-balance diff --git a/README.md b/README.md index aa47a70..632f48e 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,64 @@ # babylon-deployment +## Setup Bitcoin node + +1. Copy the `.env.bitcoin.example` file to `.env.bitcoin` and set the variables + + ```bash + cp .env.bitcoin.example .env.bitcoin + ``` + +* The `NETWORK` variable only can be either `regtest` or `signet`. +* The `BTC_PRIVKEY` variable must be a valid Bitcoin private key in WIF format. + +2. Start the Bitcoin node + + ```bash + make start-bitcoin + ``` + +3. Verify the Bitcoin node is synced and has a balance + + ```bash + make verify-bitcoin-sync-balance + ``` + +4. Stop the Bitcoin node + + ```bash + make stop-bitcoin + ``` + +5. Check the Bitcoin node logs + + ```bash + docker compose -f docker/docker-compose-bitcoin.yml logs -f bitcoind + ``` + ## Troubleshooting -1. BTC staker balance null or no unspent outputs +1. BTC wallet balance null or no unspent outputs -After running `verify-bitcoin-sync-balance.sh`, the BTC staker wallet should be loaded to bitcoind. If not, you will run into null balance or no unspent outputs errors when running `create-btc-delegations.sh`. +After running `verify-bitcoin-sync-balance.sh`, the BTC wallet should be loaded to bitcoind. If not, you will run into null balance or no unspent outputs errors when running `create-btc-delegations.sh`. To check the wallet balance: ``` -docker exec bitcoindsim /bin/sh -c "bitcoin-cli -signet -rpcuser=rpcuser -rpcpassword=rpcpass -rpcwallet=btcstaker listunspent" +docker exec bitcoind /bin/sh -c "bitcoin-cli -signet -rpcuser=rpcuser -rpcpassword=rpcpass -rpcwallet=btcwallet listunspent" ``` To check unspent outputs: ``` -docker exec bitcoindsim /bin/sh -c "bitcoin-cli -signet -rpcuser=rpcuser -rpcpassword=rpcpass -rpcwallet=btcstaker getbalance" +docker exec bitcoind /bin/sh -c "bitcoin-cli -signet -rpcuser=rpcuser -rpcpassword=rpcpass -rpcwallet=btcwallet getbalance" ``` If your wallet balance is 0 or you have no unspent outputs, you may need to re-load the wallet: ``` -docker exec bitcoindsim /bin/sh -c "bitcoin-cli -signet -rpcuser=rpcuser -rpcpassword=rpcpass -rpcwallet=btcstaker unloadwallet btcstaker" +docker exec bitcoind /bin/sh -c "bitcoin-cli -signet -rpcuser=rpcuser -rpcpassword=rpcpass -rpcwallet=btcwallet unloadwallet btcwallet" -docker exec bitcoindsim /bin/sh -c "bitcoin-cli -signet -rpcuser=rpcuser -rpcpassword=rpcpass -rpcwallet=btcstaker loadwallet btcstaker" +docker exec bitcoind /bin/sh -c "bitcoin-cli -signet -rpcuser=rpcuser -rpcpassword=rpcpass -rpcwallet=btcwallet loadwallet btcwallet" ``` -Now recheck the balance and unspent outputs. \ No newline at end of file +Now recheck the balance and unspent outputs. diff --git a/docker/docker-compose-bitcoin.yml b/docker/docker-compose-bitcoin.yml new file mode 100644 index 0000000..bce104c --- /dev/null +++ b/docker/docker-compose-bitcoin.yml @@ -0,0 +1,22 @@ +services: + bitcoind: + image: snapchain/bitcoind:d3854d9 + container_name: bitcoind + env_file: + - "${PWD}/.env.bitcoin" + ports: + - "${BITCOIN_RPC_PORT:-18443}:${BITCOIN_RPC_PORT:-18443}" + - "${ZMQ_SEQUENCE_PORT:-29000}:${ZMQ_SEQUENCE_PORT:-29000}" + - "${ZMQ_RAWBLOCK_PORT:-29001}:${ZMQ_RAWBLOCK_PORT:-29001}" + - "${ZMQ_RAWTR_PORT:-29002}:${ZMQ_RAWTR_PORT:-29002}" + volumes: + - "bitcoin_data:/bitcoind/.bitcoin" + restart: always + networks: + - default + +volumes: + bitcoin_data: + +networks: + bitcoind: \ No newline at end of file diff --git a/scripts/bitcoin/Dockerfile b/scripts/bitcoin/Dockerfile new file mode 100644 index 0000000..f9fba6b --- /dev/null +++ b/scripts/bitcoin/Dockerfile @@ -0,0 +1,22 @@ +FROM debian:bullseye-slim + +RUN useradd --system --user-group bitcoin \ + && apt-get update -y \ + && apt-get install -y curl gnupg gosu jq \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +ARG BITCOIN_CORE_VERSION="26.0" +ENV BITCOIN_CORE_VERSION=$BITCOIN_CORE_VERSION +ENV PATH=/opt/bitcoin-${BITCOIN_CORE_VERSION}/bin:$PATH + +RUN set -ex \ + && curl -SLO https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_CORE_VERSION}/bitcoin-${BITCOIN_CORE_VERSION}-x86_64-linux-gnu.tar.gz \ + && tar -xzf *.tar.gz -C /opt + +WORKDIR /bitcoind + +COPY entrypoint.sh /entrypoint.sh +COPY setup-wallet.sh /setup-wallet.sh + +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] \ No newline at end of file diff --git a/scripts/bitcoin/entrypoint.sh b/scripts/bitcoin/entrypoint.sh new file mode 100755 index 0000000..2c391c2 --- /dev/null +++ b/scripts/bitcoin/entrypoint.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -euo pipefail + +echo "NETWORK: $NETWORK" +echo "RPC_PORT: $RPC_PORT" + +if [[ "$NETWORK" != "regtest" && "$NETWORK" != "signet" ]]; then + echo "Unsupported network: $NETWORK" + exit 1 +fi + +DATA_DIR=/bitcoind/.bitcoin +CONF=/bitcoind/bitcoin.conf + +echo "Generating bitcoin.conf file at $CONF" +NETWORK_LABEL="$NETWORK" +cat < "$CONF" +# Enable ${NETWORK} mode. +${NETWORK}=1 + +# Accept command line and JSON-RPC commands +server=1 + +# RPC user and password. +rpcuser=$RPC_USER +rpcpassword=$RPC_PASS + +# ZMQ notification options. +# Enable publish hash block and tx sequence +zmqpubsequence=tcp://*:$ZMQ_SEQUENCE_PORT +# Enable publishing of raw block hex. +zmqpubrawblock=tcp://*:$ZMQ_RAWBLOCK_PORT +# Enable publishing of raw transaction. +zmqpubrawtx=tcp://*:$ZMQ_RAWTR_PORT + +txindex=1 +deprecatedrpc=create_bdb + +# Fallback fee +fallbackfee=0.00001 + +# Allow all IPs to access the RPC server. +[${NETWORK_LABEL}] +rpcbind=0.0.0.0 +rpcallowip=0.0.0.0/0 +rpcport=$RPC_PORT +EOF + +echo "Starting bitcoind..." +bitcoind -${NETWORK} -datadir="$DATA_DIR" -conf="$CONF" -rpcport="$RPC_PORT" \ No newline at end of file diff --git a/scripts/bitcoin/setup-wallet.sh b/scripts/bitcoin/setup-wallet.sh new file mode 100755 index 0000000..1348119 --- /dev/null +++ b/scripts/bitcoin/setup-wallet.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +echo "NETWORK: $NETWORK" +echo "BTC_WALLET_NAME: $BTC_WALLET_NAME" + +DATA_DIR=/bitcoind/.bitcoin + +if [[ ! -d "${DATA_DIR}/${NETWORK}/wallets/${BTC_WALLET_NAME}" ]]; then + echo "Creating a wallet ${BTC_WALLET_NAME}..." + bitcoin-cli -${NETWORK} -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" createwallet "$BTC_WALLET_NAME" false false "$BTC_WALLET_PASS" false false +fi + +echo "Opening wallet ${BTC_WALLET_NAME}..." +bitcoin-cli -${NETWORK} -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$BTC_WALLET_NAME" walletpassphrase "$BTC_WALLET_PASS" 10 +echo "Importing the private key to the wallet ${BTC_WALLET_NAME} with the label ${BTC_WALLET_NAME} without rescan..." +bitcoin-cli -${NETWORK} -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$BTC_WALLET_NAME" importprivkey "$BTC_PRIVKEY" "${BTC_WALLET_NAME}" false + +if [[ "$NETWORK" == "regtest" ]]; then + echo "Generating 110 blocks for the first coinbases to mature..." + bitcoin-cli -${NETWORK} -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$BTC_WALLET_NAME" -generate 110 + + # Waiting for the wallet to catch up. + sleep 5 + echo "Checking balance..." + bitcoin-cli -${NETWORK} -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$BTC_WALLET_NAME" getbalance + + echo "Getting the imported BTC address for wallet ${BTC_WALLET_NAME}..." + BTC_ADDR=$(bitcoin-cli -${NETWORK} -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$BTC_WALLET_NAME" getaddressesbylabel "${BTC_WALLET_NAME}" | jq -r 'keys[0]') + echo "Imported BTC address: ${BTC_ADDR}" + + if [[ -z "$GENERATE_INTERVAL_SECS" ]]; then + GENERATE_INTERVAL_SECS=600 # 10 minutes + fi + + # without it, regtest will not mine blocks + echo "Starting block generation every $GENERATE_INTERVAL_SECS seconds in the background..." + ( + while true; do + bitcoin-cli -${NETWORK} -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$BTC_WALLET_NAME" -generate 1 + sleep "$GENERATE_INTERVAL_SECS" + done + ) & +fi \ No newline at end of file diff --git a/scripts/bitcoin/start.sh b/scripts/bitcoin/start.sh new file mode 100755 index 0000000..d2e3084 --- /dev/null +++ b/scripts/bitcoin/start.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -euo pipefail + +# Load environment variables from .env.bitcoin file +set -a +source "$(pwd)/.env.bitcoin" +set +a + +# Start the bitcoin container +echo "Starting the bitcoin container..." +docker compose -f "$(pwd)/docker/docker-compose-bitcoin.yml" up -d + +# Wait for the bitcoin node to be ready +echo "Waiting for the bitcoin node to be ready..." +sleep 5 + +max_attempts=10 +attempt=0 +while ! docker exec bitcoind bitcoin-cli -${NETWORK} -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" getblockchaininfo &>/dev/null; do + sleep 2 + ((attempt++)) + if [ $attempt -ge $max_attempts ]; then + echo "Timeout waiting for bitcoin node to be ready." + exit 1 + fi +done + +echo "Bitcoin node is ready!" +echo + +# Setup the wallet +echo "Setting up the wallet..." +docker exec -it bitcoind /setup-wallet.sh +echo "Wallet setup done!" +echo \ No newline at end of file diff --git a/scripts/bitcoin/stop.sh b/scripts/bitcoin/stop.sh new file mode 100755 index 0000000..e48c988 --- /dev/null +++ b/scripts/bitcoin/stop.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail + +# Stop the bitcoin container +docker compose -f "$(pwd)/docker/docker-compose-bitcoin.yml" down + +# Remove the bitcoin volume +docker volume ls --filter name=bitcoin_data --format='{{.Name}}' | xargs -r docker volume rm \ No newline at end of file diff --git a/scripts/bitcoin/verify-sync-balance.sh b/scripts/bitcoin/verify-sync-balance.sh new file mode 100755 index 0000000..4413802 --- /dev/null +++ b/scripts/bitcoin/verify-sync-balance.sh @@ -0,0 +1,56 @@ +#!/bin/bash +set -euo pipefail + +# Load environment variables from the .env.bitcoin file +set -a +source $(pwd)/.env.bitcoin +set +a + +if [ -z "$(echo ${BTC_WALLET_PASS})" ] || [ -z "$(echo ${BTC_PRIVKEY})" ]; then + echo "Error: BTC_WALLET_PASS or BTC_PRIVKEY environment variable is not set" + exit 1 +fi +echo "Environment variables loaded successfully" +echo "NETWORK: $NETWORK" +echo "RPC_PORT: $RPC_PORT" +echo + +echo "Checking if Bitcoin node is synced..." +SYNCED=$(docker exec bitcoind /bin/sh -c " + bitcoin-cli \ + -${NETWORK} \ + -rpcuser=${RPC_USER} \ + -rpcpassword=${RPC_PASS} \ + getblockchaininfo" | jq -r '.verificationprogress') +if (( $(awk -v synced="$SYNCED" 'BEGIN {print (synced < 0.999)}') )); then + echo "Error: Bitcoin node is not fully synced. Expected at least 99.9%, got ${SYNCED}" + exit 1 +fi +echo "Bitcoin node is synced" +echo + +# Check btc address +BTC_ADDRESS=$(docker exec bitcoind /bin/sh -c " + bitcoin-cli \ + -${NETWORK} \ + -rpcuser=${RPC_USER} \ + -rpcpassword=${RPC_PASS} \ + -rpcwallet=${BTC_WALLET_NAME} \ + getaddressesbylabel \"${BTC_WALLET_NAME}\"" \ + | jq -r 'keys[0]') +echo "BTC address: ${BTC_ADDRESS}" + +# Check if btc has any unspent transactions +BALANCE_BTC=$(docker exec bitcoind /bin/sh -c " + bitcoin-cli \ + -${NETWORK} \ + -rpcuser=${RPC_USER} \ + -rpcpassword=${RPC_PASS} \ + -rpcwallet=${BTC_WALLET_NAME} \ + listunspent" | jq -r '[.[] | .amount] | add') +if (( $(awk -v balance="$BALANCE_BTC" 'BEGIN {print (balance < 0.01)}') )); then + echo "Warning: BTC balance is less than 0.01 BTC. You may need to fund this address for ${NETWORK}." +else + echo "BTC balance is sufficient: ${BALANCE_BTC} BTC" +fi +echo \ No newline at end of file