Files
nym/docker/localnet/localnet.sh
T
Jędrzej Stuczyński e2dd8ac743 feat: localnet v2 (#6277)
* squashing localnet-v2 commits (again)

cargo fmt

fixes to localnet purge

provide path in the error message

output args

log failed exec

print based on tty

check-prerequisites cmd

checked iptables update

basic kernel features check

enable ipv6 rules

add forwarding rules

squashing localnet-v2 commits

additional changes

propagate custom-dns flag to all run containers

remove is_mock from EcashManager

another localnet squash

unused import

chore: remove redundant testnet manager

missing impl

additional linux fixes

command to rebuild container image

wait for at least 2 blocks

additional node startup fixes

added --custom-dns flag to nym node setup

add gateway probe + wait for DKG magic file

fixed localnet down on linux

container ls

re-enable state resync

additional feature locking

macos adjustments

working nyxd startup on linux

wip linux box

wip

separating network inspect betweewn macos and linux

initial linux feature locking

moved all container commands into a single location

finally working initial node performance

squashing orchestrator commits

cleanup

fixed condition for naive rearrangement

added cache of cosmwasm contracts for speed up on subsequent runs

'down' command

refreshing described cache after nodes are bonded

nym nodes setup + wip on nym api refresh

nodes setup WIP

first pass cleanup

placeholder for nym-node setup

bypassing the dkg

further progress on nym-api setup

wip: api setup

up/down/purge placeholders

persisting contract setup data

fix contract upload by forcing amd64 container platform

wip: contracts setup4

wip: contracts setup3

wip: contracts setup2

wip: contracts setup

include network setup

init and spawn nyxd

build nyxd image in dedicated orchestrator

build nyxd image

squashed cherry-picked lp changes

Bits and bobs to make everything work

Title

MacOS setup instructions

Docker/Container localnet

* clippy

* fixes on non-unix targets

---------

Co-authored-by: durch <durch@users.noreply.github.com>
2026-03-12 14:46:00 +00:00

714 lines
21 KiB
Bash
Executable File

#!/bin/bash
set -ex
# Nym Localnet Orchestration Script for Apple Container Runtime
# Emulates docker-compose functionality
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
IMAGE_NAME="nym-localnet:latest"
VOLUME_NAME="nym-localnet-data"
VOLUME_PATH="/tmp/nym-localnet-$$"
NYM_VOLUME_PATH="/tmp/nym-localnet-home-$$"
SUFFIX=${NYM_NODE_SUFFIX:-localnet}
# Container names
INIT_CONTAINER="nym-localnet-init"
MIXNODE1_CONTAINER="nym-mixnode1"
MIXNODE2_CONTAINER="nym-mixnode2"
MIXNODE3_CONTAINER="nym-mixnode3"
GATEWAY_CONTAINER="nym-gateway"
GATEWAY2_CONTAINER="nym-gateway2"
REQUESTER_CONTAINER="nym-network-requester"
SOCKS5_CONTAINER="nym-socks5-client"
ALL_CONTAINERS=(
"$MIXNODE1_CONTAINER"
"$MIXNODE2_CONTAINER"
"$MIXNODE3_CONTAINER"
"$GATEWAY_CONTAINER"
"$GATEWAY2_CONTAINER"
"$REQUESTER_CONTAINER"
"$SOCKS5_CONTAINER"
)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() {
echo -e "${BLUE}[INFO]${NC} $*"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $*"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*"
}
cleanup_host_state() {
log_info "Cleaning local nym-node state for suffix ${SUFFIX}"
for node in mix1 mix2 mix3 gateway gateway2; do
rm -rf "$HOME/.nym/nym-nodes/${node}-${SUFFIX}"
done
}
# Check if container command exists
check_prerequisites() {
if ! command -v container &> /dev/null; then
log_error "Apple 'container' command not found"
log_error "Install from: https://github.com/apple/container"
exit 1
fi
}
# Build the Docker image
build_image() {
log_info "Building image: $IMAGE_NAME"
log_warn "This will take 15-30 minutes on first build..."
cd "$PROJECT_ROOT"
# Build with Docker
log_info "Building with Docker..."
if ! docker build \
-f "$SCRIPT_DIR/Dockerfile.localnet" \
-t "$IMAGE_NAME" \
"$PROJECT_ROOT"; then
log_error "Docker build failed"
exit 1
fi
# Transfer image to container runtime
log_info "Transferring image to container runtime..."
# Save to temporary file (container image load doesn't support stdin)
TEMP_IMAGE="/tmp/nym-localnet-image-$$.tar"
if ! docker save -o "$TEMP_IMAGE" "$IMAGE_NAME"; then
log_error "Failed to save Docker image"
exit 1
fi
# Load into container runtime from file
if ! container image load --input "$TEMP_IMAGE"; then
rm -f "$TEMP_IMAGE"
log_error "Failed to load image into container runtime"
exit 1
fi
# Clean up temporary file
rm -f "$TEMP_IMAGE"
# Verify image is available
if ! container image inspect "$IMAGE_NAME" &>/dev/null; then
log_error "Image not found in container runtime after load"
exit 1
fi
log_success "Image built and loaded: $IMAGE_NAME"
}
# Create shared volume directory
create_volume() {
log_info "Creating shared volume at: $VOLUME_PATH"
mkdir -p "$VOLUME_PATH"
chmod 777 "$VOLUME_PATH"
log_success "Volume created"
}
# Create shared nym home directory
create_nym_volume() {
log_info "Creating shared nym home volume at: $NYM_VOLUME_PATH"
mkdir -p "$NYM_VOLUME_PATH"
chmod 777 "$NYM_VOLUME_PATH"
log_success "Nym home volume created"
}
# Remove shared volume directory
remove_volume() {
if [ -d "$VOLUME_PATH" ]; then
log_info "Removing volume: $VOLUME_PATH"
rm -rf "$VOLUME_PATH"
log_success "Volume removed"
fi
if [ -d "$NYM_VOLUME_PATH" ]; then
log_info "Removing nym home volume: $NYM_VOLUME_PATH"
rm -rf "$NYM_VOLUME_PATH"
log_success "Nym home volume removed"
fi
}
# Network name
NETWORK_NAME="nym-localnet-network"
# Create container network
create_network() {
log_info "Creating container network: $NETWORK_NAME"
if container network create "$NETWORK_NAME" 2>/dev/null; then
log_success "Network created: $NETWORK_NAME"
else
log_info "Network $NETWORK_NAME already exists or creation failed"
fi
}
# Remove container network
remove_network() {
if container network list | grep -q "$NETWORK_NAME"; then
log_info "Removing network: $NETWORK_NAME"
container network rm "$NETWORK_NAME" 2>/dev/null || true
log_success "Network removed"
fi
}
# Start a mixnode
start_mixnode() {
local node_id=$1
local container_name=$2
log_info "Starting $container_name..."
# Calculate port numbers based on node_id
local mixnet_port="1000${node_id}"
local verloc_port="2000${node_id}"
local http_port="3000${node_id}"
container run \
--name "$container_name" \
--dns 1.1.1.1 \
-m 2G \
--network "$NETWORK_NAME" \
-p "${mixnet_port}:${mixnet_port}" \
-p "${verloc_port}:${verloc_port}" \
-p "${http_port}:${http_port}" \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
-d \
-e "NYM_NODE_SUFFIX=$SUFFIX" \
"$IMAGE_NAME" \
sh -c '
CONTAINER_IP=$(hostname -i);
echo "Container IP: $CONTAINER_IP";
echo "Initializing mix'"${node_id}"'...";
nym-node run --id mix'"${node_id}"'-localnet --init-only \
--unsafe-disable-replay-protection \
--local \
--mixnet-bind-address=0.0.0.0:'"${mixnet_port}"' \
--verloc-bind-address=0.0.0.0:'"${verloc_port}"' \
--http-bind-address=0.0.0.0:'"${http_port}"' \
--http-access-token=lala \
--public-ips $CONTAINER_IP \
--output=json \
--bonding-information-output="/localnet/mix'"${node_id}"'.json";
echo "Waiting for network.json...";
while [ ! -f /localnet/network.json ]; do
sleep 2;
done;
echo "Starting mix'"${node_id}"'...";
exec nym-node run --id mix'"${node_id}"'-localnet --unsafe-disable-replay-protection --local
'
log_success "$container_name started"
}
# Start gateway
start_gateway() {
log_info "Starting $GATEWAY_CONTAINER..."
container run \
--name "$GATEWAY_CONTAINER" \
--dns 1.1.1.1 \
-m 2G \
--network "$NETWORK_NAME" \
-p 9000:9000 \
-p 10004:10004 \
-p 20004:20004 \
-p 30004:30004 \
-p 41264:41264 \
-p 51264:51264 \
-p 51822:51822/udp \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
-d \
-e "NYM_NODE_SUFFIX=$SUFFIX" \
"$IMAGE_NAME" \
sh -c '
CONTAINER_IP=$(hostname -i);
echo "Container IP: $CONTAINER_IP";
echo "Initializing gateway...";
nym-node run --id gateway-localnet --init-only \
--unsafe-disable-replay-protection \
--local \
--mode entry-gateway \
--mode exit-gateway \
--mixnet-bind-address=0.0.0.0:10004 \
--entry-bind-address=0.0.0.0:9000 \
--verloc-bind-address=0.0.0.0:20004 \
--http-bind-address=0.0.0.0:30004 \
--http-access-token=lala \
--public-ips $CONTAINER_IP \
--enable-lp true \
--lp-use-mock-ecash true \
--output=json \
--wireguard-enabled true \
--wireguard-userspace true \
--bonding-information-output="/localnet/gateway.json";
echo "Waiting for network.json...";
while [ ! -f /localnet/network.json ]; do
sleep 2;
done;
echo "Starting gateway with LP listener (mock ecash)...";
exec nym-node run --id gateway-localnet --unsafe-disable-replay-protection --local --wireguard-enabled true --wireguard-userspace true --lp-use-mock-ecash true
'
log_success "$GATEWAY_CONTAINER started"
# Wait for gateway to be ready
log_info "Waiting for gateway to listen on port 9000..."
local retries=0
local max_retries=30
while ! nc -z 127.0.0.1 9000 2>/dev/null; do
sleep 2
retries=$((retries + 1))
if [ $retries -ge $max_retries ]; then
log_error "Gateway failed to start on port 9000"
return 1
fi
done
log_success "Gateway is ready on port 9000"
}
# Start gateway2
start_gateway2() {
log_info "Starting $GATEWAY2_CONTAINER..."
container run \
--name "$GATEWAY2_CONTAINER" \
-m 2G \
--network "$NETWORK_NAME" \
-p 9001:9001 \
-p 10005:10005 \
-p 20005:20005 \
-p 30005:30005 \
-p 41265:41265 \
-p 51265:51265 \
-p 51823:51822/udp \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
-d \
-e "NYM_NODE_SUFFIX=$SUFFIX" \
"$IMAGE_NAME" \
sh -c '
CONTAINER_IP=$(hostname -i);
echo "Container IP: $CONTAINER_IP";
echo "Initializing gateway2...";
nym-node run --id gateway2-localnet --init-only \
--unsafe-disable-replay-protection \
--local \
--mode entry-gateway \
--mode exit-gateway \
--mixnet-bind-address=0.0.0.0:10005 \
--entry-bind-address=0.0.0.0:9001 \
--verloc-bind-address=0.0.0.0:20005 \
--http-bind-address=0.0.0.0:30005 \
--http-access-token=lala \
--public-ips $CONTAINER_IP \
--enable-lp true \
--lp-use-mock-ecash true \
--output=json \
--wireguard-enabled true \
--wireguard-userspace true \
--bonding-information-output="/localnet/gateway2.json";
echo "Waiting for network.json...";
while [ ! -f /localnet/network.json ]; do
sleep 2;
done;
echo "Starting gateway2 with LP listener (mock ecash)...";
exec nym-node run --id gateway2-localnet --unsafe-disable-replay-protection --local --wireguard-enabled true --wireguard-userspace true --lp-use-mock-ecash true
'
log_success "$GATEWAY2_CONTAINER started"
# Wait for gateway2 to be ready
log_info "Waiting for gateway2 to listen on port 9001..."
local retries=0
local max_retries=30
while ! nc -z 127.0.0.1 9001 2>/dev/null; do
sleep 2
retries=$((retries + 1))
if [ $retries -ge $max_retries ]; then
log_error "Gateway2 failed to start on port 9001"
return 1
fi
done
log_success "Gateway2 is ready on port 9001"
}
# Start network requester
start_network_requester() {
log_info "Starting $REQUESTER_CONTAINER..."
# Get gateway IP address
log_info "Getting gateway IP address..."
GATEWAY_IP=$(container exec "$GATEWAY_CONTAINER" hostname -i)
log_info "Gateway IP: $GATEWAY_IP"
container run \
--name "$REQUESTER_CONTAINER" \
--dns 1.1.1.1 \
--network "$NETWORK_NAME" \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
-e "GATEWAY_IP=$GATEWAY_IP" \
-d \
"$IMAGE_NAME" \
sh -c '
while [ ! -f /localnet/network.json ]; do
echo "Waiting for network.json...";
sleep 2;
done;
while ! nc -z $GATEWAY_IP 9000 2>/dev/null; do
echo "Waiting for gateway on port 9000 ($GATEWAY_IP)...";
sleep 2;
done;
SUFFIX=$(date +%s);
nym-network-requester init \
--id "network-requester-$SUFFIX" \
--open-proxy=true \
--custom-mixnet /localnet/network.json \
--output=json > /localnet/network_requester.json;
exec nym-network-requester run \
--id "network-requester-$SUFFIX" \
--custom-mixnet /localnet/network.json
'
log_success "$REQUESTER_CONTAINER started"
}
# Start SOCKS5 client
start_socks5_client() {
log_info "Starting $SOCKS5_CONTAINER..."
container run \
--name "$SOCKS5_CONTAINER" \
--dns 1.1.1.1 \
--network "$NETWORK_NAME" \
-p 1080:1080 \
-v "$VOLUME_PATH:/localnet:ro" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
-d \
"$IMAGE_NAME" \
sh -c '
while [ ! -f /localnet/network_requester.json ]; do
echo "Waiting for network requester...";
sleep 2;
done;
SUFFIX=$(date +%s);
PROVIDER=$(cat /localnet/network_requester.json | grep -o "\"client_address\":\"[^\"]*\"" | cut -d\" -f4);
if [ -z "$PROVIDER" ]; then
echo "Error: Could not extract provider address";
exit 1;
fi;
nym-socks5-client init \
--id "socks5-client-$SUFFIX" \
--provider "$PROVIDER" \
--custom-mixnet /localnet/network.json \
--no-cover;
exec nym-socks5-client run \
--id "socks5-client-$SUFFIX" \
--custom-mixnet /localnet/network.json \
--host 0.0.0.0
'
log_success "$SOCKS5_CONTAINER started"
# Wait for SOCKS5 to be ready
log_info "Waiting for SOCKS5 proxy on port 1080..."
sleep 5
local retries=0
local max_retries=15
while ! nc -z 127.0.0.1 1080 2>/dev/null; do
sleep 2
retries=$((retries + 1))
if [ $retries -ge $max_retries ]; then
log_warn "SOCKS5 proxy not responding on port 1080 yet"
return 0
fi
done
log_success "SOCKS5 proxy is ready on port 1080"
}
# Stop all containers
stop_containers() {
log_info "Stopping all containers..."
for container_name in "${ALL_CONTAINERS[@]}"; do
if container inspect "$container_name" &>/dev/null; then
log_info "Stopping $container_name"
container stop "$container_name" 2>/dev/null || true
container rm "$container_name" 2>/dev/null || true
fi
done
# Also clean up init container if it exists
container rm "$INIT_CONTAINER" 2>/dev/null || true
log_success "All containers stopped"
cleanup_host_state
remove_network
}
# Show container logs
show_logs() {
local container_name=${1:-}
if [ -z "$container_name" ]; then
# No container specified - launch tmux log viewer
log_info "Launching tmux log viewer for all containers..."
exec "$SCRIPT_DIR/localnet-logs.sh"
fi
# Show logs for specific container
if container inspect "$container_name" &>/dev/null; then
container logs -f "$container_name"
else
log_error "Container not found: $container_name"
log_info "Available containers:"
for name in "${ALL_CONTAINERS[@]}"; do
echo " - $name"
done
exit 1
fi
}
# Show container status
show_status() {
log_info "Container status:"
echo ""
for container_name in "${ALL_CONTAINERS[@]}"; do
if container inspect "$container_name" &>/dev/null; then
local status=$(container inspect "$container_name" 2>/dev/null | grep -o '"Status":"[^"]*"' | cut -d'"' -f4 || echo "unknown")
echo -e " ${GREEN}${NC} $container_name - $status"
else
echo -e " ${RED}${NC} $container_name - not running"
fi
done
echo ""
log_info "Port status:"
echo " Mixnet:"
for port in 10001 10002 10003 10004; do
if nc -z 127.0.0.1 $port 2>/dev/null; then
echo -e " ${GREEN}${NC} Port $port - listening"
else
echo -e " ${RED}${NC} Port $port - not listening"
fi
done
echo " Gateway:"
for port in 9000 30004; do
if nc -z 127.0.0.1 $port 2>/dev/null; then
echo -e " ${GREEN}${NC} Port $port - listening"
else
echo -e " ${RED}${NC} Port $port - not listening"
fi
done
echo " LP (Lewes Protocol):"
for port in 41264 51264; do
if nc -z 127.0.0.1 $port 2>/dev/null; then
echo -e " ${GREEN}${NC} Port $port - listening"
else
echo -e " ${RED}${NC} Port $port - not listening"
fi
done
echo " SOCKS5:"
if nc -z 127.0.0.1 1080 2>/dev/null; then
echo -e " ${GREEN}${NC} Port 1080 - listening"
else
echo -e " ${RED}${NC} Port 1080 - not listening"
fi
}
# Build network topology with container IPs
build_topology() {
log_info "Building network topology with container IPs..."
# Wait for all bonding JSON files to be created
log_info "Waiting for all nodes to complete initialization..."
for file in mix1.json mix2.json mix3.json gateway.json gateway2.json; do
while [ ! -f "$VOLUME_PATH/$file" ]; do
echo " Waiting for $file..."
sleep 1
done
log_success " $file created"
done
# Get container IPs
log_info "Getting container IP addresses..."
MIX1_IP=$(container exec "$MIXNODE1_CONTAINER" hostname -i)
MIX2_IP=$(container exec "$MIXNODE2_CONTAINER" hostname -i)
MIX3_IP=$(container exec "$MIXNODE3_CONTAINER" hostname -i)
GATEWAY_IP=$(container exec "$GATEWAY_CONTAINER" hostname -i)
GATEWAY2_IP=$(container exec "$GATEWAY2_CONTAINER" hostname -i)
log_info "Container IPs:"
echo " mix1: $MIX1_IP"
echo " mix2: $MIX2_IP"
echo " mix3: $MIX3_IP"
echo " gateway: $GATEWAY_IP"
echo " gateway2: $GATEWAY2_IP"
# Run build_topology.py in a container with access to the volumes
container run \
--name "nym-localnet-topology-builder" \
--dns 1.1.1.1 \
--network "$NETWORK_NAME" \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
--rm \
"$IMAGE_NAME" \
python3 /usr/local/bin/build_topology.py \
/localnet \
"$SUFFIX" \
"$MIX1_IP" \
"$MIX2_IP" \
"$MIX3_IP" \
"$GATEWAY_IP" \
"$GATEWAY2_IP"
# Verify network.json was created
if [ -f "$VOLUME_PATH/network.json" ]; then
log_success "Network topology created successfully"
else
log_error "Failed to create network topology"
exit 1
fi
}
# Start all services
start_all() {
log_info "Starting Nym Localnet..."
cleanup_host_state
create_network
create_volume
create_nym_volume
start_mixnode 1 "$MIXNODE1_CONTAINER"
start_mixnode 2 "$MIXNODE2_CONTAINER"
start_mixnode 3 "$MIXNODE3_CONTAINER"
start_gateway
start_gateway2
build_topology
# Configure networking for two-hop WireGuard routing on both gateways
# Note: Runs after build_topology to ensure gateways have finished WireGuard setup
log_info "Configuring gateway networking (IP forwarding, NAT)..."
for gw in "$GATEWAY_CONTAINER" "$GATEWAY2_CONTAINER"; do
container exec "$gw" sh -c "
# Enable IP forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
# Add NAT masquerade for outbound traffic
iptables-legacy -t nat -A POSTROUTING -o eth0 -j MASQUERADE
"
log_success "Configured $gw"
done
start_network_requester
start_socks5_client
echo ""
log_success "Nym Localnet is running!"
echo ""
echo "Test with:"
echo " curl -x socks5h://127.0.0.1:1080 https://nymtech.net"
echo ""
echo "View logs:"
echo " $0 logs # All containers in tmux"
echo " $0 logs gateway # Single container"
echo ""
echo "Stop:"
echo " $0 down"
echo ""
}
# Main command handler
main() {
check_prerequisites
local command=${1:-help}
shift || true
case "$command" in
build)
build_image
;;
up)
build_image
start_all
;;
start)
start_all
;;
down|stop)
stop_containers
remove_volume
;;
restart)
stop_containers
start_all
;;
logs)
show_logs "$@"
;;
status|ps)
show_status
;;
help|--help|-h)
cat <<EOF
Nym Localnet Orchestration Script
Usage: $0 <command> [options]
Commands:
build Build the localnet image
up Build image and start all services
start Start all services (requires built image)
down, stop Stop all services and clean up
restart Restart all services
logs [name] Show logs (no args = tmux overlay, with name = single container)
status, ps Show status of all containers and ports
help Show this help message
Examples:
$0 up # Build and start everything
$0 logs # View all logs in tmux overlay
$0 logs gateway # View gateway logs only
$0 status # Check what's running
$0 down # Stop and clean up
EOF
;;
*)
log_error "Unknown command: $command"
echo "Run '$0 help' for usage information"
exit 1
;;
esac
}
main "$@"