diff --git a/.env.signet b/.env.signet index c4080f0..dd2c980 100644 --- a/.env.signet +++ b/.env.signet @@ -7,7 +7,7 @@ CORE_RPC_ADDR=mainchain:38332 CORE_P2P_ADDR=mainchain:38333 CORE_ZMQ_ADDR_SEQUENCE=tcp://mainchain:29000 -SIGNET_MINER_COINBASE_RECIPIENT=tb1qnwwylelpqjmlwr4n53am4nn8jzr3nl3zswjzg4 -SIGNET_CHALLENGE=a91484fa7c2460891fe5212cb08432e21a4207909aa987 - -NETWORK_MAGIC=e409e968 \ No newline at end of file +SIGNET_VERSION=20260608121741 +SIGNET_MINER_COINBASE_RECIPIENT=tb1qlt5ryglszav4stl7wr6lwu8ts330qndztuzve7 +SIGNET_CHALLENGE=00148835832e28c816b7acd8fdb19772ab2199603a56 +NETWORK_MAGIC=13182df0 diff --git a/docker-compose.base.yml b/docker-compose.base.yml index 20c4a50..650d6ee 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -22,7 +22,7 @@ services: - source: bitcoind-${NETWORK}-cookie target: /cookie - image: ghcr.io/layertwo-labs/bitcoin-patched:sha-32e384a + image: ghcr.io/layertwo-labs/bitcoin-patched:v30.2 restart: unless-stopped ports: - "38333:38333" # P2P, exposed to the internet @@ -34,6 +34,11 @@ services: - -server - -chain=${NETWORK} - -signetchallenge=${SIGNET_CHALLENGE:-""} + # This `mainchain` service only runs under the signet profile (forknet uses + # an external node). A self-mined signet has backdated/sparse blocks, so + # without a large maxtipage the node sits in initial-block-download forever + # and electrs never goes healthy. Leave IBD decided on chainwork alone. + - -maxtipage=2147483647 - -acceptnonstdtxn - -listen - -rpcbind=0.0.0.0 @@ -44,7 +49,7 @@ services: - -rest enforcer: - image: ghcr.io/layertwo-labs/bip300301_enforcer:sha-a57e6f1 + image: ghcr.io/layertwo-labs/bip300301_enforcer:sha-a5bdc84 pull_policy: always restart: unless-stopped environment: @@ -415,7 +420,7 @@ services: faucet-backend: restart: unless-stopped # Find image-tags here: https://github.com/LayerTwo-Labs/faucet-backend/pkgs/container/faucet-backend - image: ghcr.io/layertwo-labs/faucet-backend:pr-1682 + image: ghcr.io/layertwo-labs/faucet-backend:sha-75219b6 pull_policy: always healthcheck: test: @@ -465,7 +470,7 @@ services: # Find image-tags here: # Signet: https://github.com/LayerTwo-Labs/faucet-frontend-signet-server/pkgs/container/faucet-frontend # Forknet: https://github.com/LayerTwo-Labs/faucet-frontend-forknet-server/pkgs/container/faucet-frontend - image: ghcr.io/layertwo-labs/faucet-frontend-${NETWORK}-server:pr-1682 + image: ghcr.io/layertwo-labs/faucet-frontend-${NETWORK}-server:sha-75219b6 # Can be used to run a container with the same volumes as the other services. # $ docker compose run --rm busybox sh diff --git a/docker-compose.signet.yml b/docker-compose.signet.yml index bc356f6..2e360db 100644 --- a/docker-compose.signet.yml +++ b/docker-compose.signet.yml @@ -1,12 +1,13 @@ -name: signet-server-20260205 +# Project name carries the signet version, so each version gets its own +# (project-scoped) data volume -- a new version is a clean slate, no wipe needed. +name: signet-server-${SIGNET_VERSION} services: mainchain: profiles: - signet - # map the named volume to the data directory - volumes: [mainchain-data:/home/drivechain/.drivechain-20260205] + volumes: [mainchain-data:/home/drivechain/.drivechain] enforcer: profiles: @@ -79,14 +80,3 @@ secrets: bitcoind-signet-rpcauth.conf: file: /data/secrets/bitcoind-signet-rpcauth.conf - -volumes: - # Paths in volume mounts work poorly with remote Docker contexts. - # We therefore specify external volumes. These can be created with - # the following command on the machine hosting the Docker context: - # $ docker volume create \ - # --driver local --opt type=none \ - # --opt o=bind --opt device=/data/mainchain-signet \ - # mainchain-signet-data - mainchain-signet-data: - external: true diff --git a/justfile b/justfile index 3d77679..23058c4 100644 --- a/justfile +++ b/justfile @@ -11,6 +11,64 @@ compose-signet *args="": -f docker-compose.base.yml \ -f docker-compose.signet.yml {{ args }} +# Bootstrap a freshly generated signet onto the remote node, end to end +bootstrap-signet gen_dir: + #! /usr/bin/env bash + set -euo pipefail + gen_dir="{{ gen_dir }}"; gen_dir="${gen_dir%/}" + gen_env="$gen_dir/env.signet" + wallet="$gen_dir/signet-miner.dat" + [ -f "$gen_env" ] || { echo "❌ no env.signet in $gen_dir"; exit 1; } + [ -f "$wallet" ] || { echo "❌ no signet-miner.dat in $gen_dir"; exit 1; } + + for key in SIGNET_VERSION SIGNET_CHALLENGE NETWORK_MAGIC SIGNET_MINER_COINBASE_RECIPIENT; do + if [ "$(grep "^$key=" "$gen_env")" != "$(grep "^$key=" .env.signet)" ]; then + echo "❌ $key in .env.signet does not match $gen_env" + echo " copy all four values from $gen_env into .env.signet, then retry" + exit 1 + fi + done + + compose=(docker --context l2l-signet compose --env-file .env.signet --profile signet + -f docker-compose.base.yml -f docker-compose.signet.yml) + current="signet-server-$(grep '^SIGNET_VERSION=' .env.signet | cut -d= -f2)" + + # Remove containers from any older signet-server-* projects (their data volumes are left intact). + for proj in $(docker --context l2l-signet compose ls --all --format json 2>/dev/null \ + | jq -r '.[].Name' | grep '^signet-server-' | grep -vx "$current" || true); do + echo ">> removing containers from older version $proj" + ids="$(docker --context l2l-signet ps -aq --filter "label=com.docker.compose.project=$proj")" + [ -z "$ids" ] || docker --context l2l-signet rm -f $ids >/dev/null + done + + echo ">> starting mainchain (project $current)" + "${compose[@]}" up -d mainchain + for i in $(seq 1 60); do + "${compose[@]}" exec -T mainchain drivechain-cli -rpccookiefile=/cookie -chain=signet getblockchaininfo >/dev/null 2>&1 && break + [ "$i" = 60 ] && { echo "❌ mainchain RPC never came up"; "${compose[@]}" logs --tail 20 mainchain; exit 1; } + sleep 1 + done + + echo ">> loading wallet" + "${compose[@]}" cp "$wallet" mainchain:/tmp/wallet.dat + "${compose[@]}" exec -u root -T mainchain chmod a+r /tmp/wallet.dat + "${compose[@]}" exec -T mainchain drivechain-cli -rpccookiefile=/cookie -chain=signet restorewallet signet-miner /tmp/wallet.dat true + "${compose[@]}" exec -T mainchain rm -f /tmp/wallet.dat + + echo ">> bringing up the rest of the stack" + "${compose[@]}" up -d + + echo ">> waiting for enforcer, then mining the first block" + for i in $(seq 1 90); do + [ -n "$(docker --context l2l-signet ps -q --filter "name=$current-enforcer-1" --filter health=healthy)" ] && break + [ "$i" = 90 ] && { echo "❌ enforcer never became healthy"; exit 1; } + sleep 2 + done + "${compose[@]}" run --rm buf curl --protocol grpc --http2-prior-knowledge \ + --data '{"blocks":1,"ackAllProposals":true}' \ + http://enforcer:50051/cusf.mainchain.v1.WalletService/GenerateBlocks + echo "✅ signet bootstrapped and mining" + compose-forknet *args="": #! /usr/bin/env bash if [ docker context inspect l2l-forknet > /dev/null 2>&1 ]; then diff --git a/scripts/new-signet.sh b/scripts/new-signet.sh new file mode 100755 index 0000000..9e37a48 --- /dev/null +++ b/scripts/new-signet.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# +# Bootstrap a new signet. Uses the SAME drivechaind image as the mainchain +# service so the wallet file is byte-format compatible with the production node. +# +# Bitcoin Core stamps a wallet's SQLite application_id with the network magic, so +# a wallet only loads on a node running the matching -signetchallenge. The +# challenge is itself derived from the wallet, so this runs in two phases: +# phase 1: default signet -> create a wallet, learn its challenge + magic +# phase 2: a node running THAT challenge -> rebuild the wallet (import the key) +# so its application_id matches the new network's magic +# The phase-2 wallet file is the artifact; SIGNET_CHALLENGE / NETWORK_MAGIC / +# SIGNET_MINER_COINBASE_RECIPIENT are written to env.signet. +# +# To deploy onto the mainchain node (it signs the blocks): +# 1. copy all four values from /env.signet into .env.signet (SIGNET_VERSION +# drives the compose project name, so a new version = a fresh data volume). +# 2. just bootstrap-signet +# (starts mainchain on the new challenge, loads the wallet, brings up the stack) +set -euo pipefail + +usage() { + echo "usage: scripts/new-signet.sh [OUTPUT_DIR]" +} + +# Network magic = first 4 bytes of dSHA256(CompactSize(len) || challenge). +magic_from_challenge() { + local challenge="$1" len cs + len=$(( ${#challenge} / 2 )) + cs=$(printf '%02x' "$len") + printf '%s' "${cs}${challenge}" \ + | xxd -r -p \ + | sha256sum | cut -d' ' -f1 | xxd -r -p \ + | sha256sum | cut -d' ' -f1 | cut -c1-8 +} + +OUT="" +WALLET="signet-miner" + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage; exit 0 ;; + -*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;; + *) OUT="$1"; shift ;; + esac +done + +missing=() +for bin in docker jq xxd sha256sum; do + command -v "$bin" >/dev/null 2>&1 || missing+=("$bin") +done +[[ ${#missing[@]} -eq 0 ]] || { echo "error: missing required binaries: ${missing[*]}" >&2; exit 1; } + +# Digits only: also used as SIGNET_VERSION -> the compose project name. +version="$(date -u +%Y%m%d%H%M%S)" +[[ -n "$OUT" ]] || OUT="signet-$version" +[[ ! -e "$OUT" ]] || { echo "error: output path already exists: $OUT" >&2; exit 1; } + +# Pin to the same node image the mainchain service uses, so wallet files match. +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +image="$(grep -m1 -E '^[[:space:]]*image:[[:space:]]*ghcr.io/layertwo-labs/bitcoin-patched' \ + "$repo_root/docker-compose.base.yml" | awk '{print $2}')" +[[ -n "$image" ]] || { echo "error: bitcoin-patched image not found in docker-compose.base.yml" >&2; exit 1; } + +datadir=/home/drivechain/.drivechain +C1="signet-gen1-$$" +C2="signet-gen2-$$" +trap 'docker rm -f "$C1" "$C2" >/dev/null 2>&1 || true' EXIT + +# Start an offline node (no peers, no sync) and wait for RPC. $1=name, rest=extra args. +start_node() { + local name="$1"; shift + docker run -d --name "$name" "$image" \ + drivechaind -signet "-datadir=$datadir" -connect=0 -listen=0 -dnsseed=0 "$@" >/dev/null + local i + for i in $(seq 1 120); do + docker exec "$name" drivechain-cli -signet "-datadir=$datadir" getblockchaininfo >/dev/null 2>&1 && return 0 + sleep 0.5 + done + echo "error: node $name did not become ready" >&2 + docker logs --tail 20 "$name" >&2 + return 1 +} + +# --- phase 1: default signet, learn the challenge and grab the key --- +start_node "$C1" +G1=(docker exec "$C1" drivechain-cli -signet "-datadir=$datadir" -rpcwallet=gen) +docker exec "$C1" drivechain-cli -signet "-datadir=$datadir" -named createwallet wallet_name=gen >/dev/null +challenge_addr="$("${G1[@]}" getnewaddress signet-challenge)" +challenge="$("${G1[@]}" getaddressinfo "$challenge_addr" | jq -r .scriptPubKey)" +coinbase_addr="$("${G1[@]}" getnewaddress miner-payout)" +descriptors="$("${G1[@]}" listdescriptors true \ + | jq -c '[.descriptors[] | {desc, active, internal, range, timestamp:"now"} | with_entries(select(.value != null))]')" +magic="$(magic_from_challenge "$challenge")" +docker rm -f "$C1" >/dev/null 2>&1 + +# --- phase 2: a node running THIS challenge, so the wallet's app_id matches --- +start_node "$C2" -signetchallenge="$challenge" +G2=(docker exec "$C2" drivechain-cli -signet "-datadir=$datadir" -rpcwallet="$WALLET") +docker exec "$C2" drivechain-cli -signet "-datadir=$datadir" \ + -named createwallet wallet_name="$WALLET" blank=true >/dev/null +imported="$("${G2[@]}" importdescriptors "$descriptors")" +echo "$imported" | jq -e 'all(.[]; .success)' >/dev/null \ + || { echo "error: importdescriptors failed: $imported" >&2; exit 1; } + +# Shut down so the wallet db is flushed, then copy it out of the container. +"${G2[@]}" stop >/dev/null +docker wait "$C2" >/dev/null + +mkdir -p "$OUT" +WALLET_FILE="$OUT/$WALLET.dat" +docker cp "$C2:$datadir/signet/wallets/$WALLET/wallet.dat" "$WALLET_FILE" >/dev/null +echo "wrote wallet: $WALLET_FILE" >&2 + +printf 'SIGNET_VERSION=%s\nSIGNET_MINER_COINBASE_RECIPIENT=%s\nSIGNET_CHALLENGE=%s\nNETWORK_MAGIC=%s\n' \ + "$version" "$coinbase_addr" "$challenge" "$magic" > "$OUT/env.signet" +echo "wrote network params: $OUT/env.signet" >&2