Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .env.signet
Original file line number Diff line number Diff line change
Expand Up @@ -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
SIGNET_VERSION=20260608121741
SIGNET_MINER_COINBASE_RECIPIENT=tb1qlt5ryglszav4stl7wr6lwu8ts330qndztuzve7
SIGNET_CHALLENGE=00148835832e28c816b7acd8fdb19772ab2199603a56
NETWORK_MAGIC=13182df0
13 changes: 9 additions & 4 deletions docker-compose.base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
18 changes: 4 additions & 14 deletions docker-compose.signet.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
58 changes: 58 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
117 changes: 117 additions & 0 deletions scripts/new-signet.sh
Original file line number Diff line number Diff line change
@@ -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 <out>/env.signet into .env.signet (SIGNET_VERSION
# drives the compose project name, so a new version = a fresh data volume).
# 2. just bootstrap-signet <out>
# (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
Loading