Skip to content
Open
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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.1
0.6.0
2 changes: 1 addition & 1 deletion docs/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Some Logos Blockchain releases reset the genesis block or otherwise make existin
2. Backs up `~/.logos-node/user_config.yaml` to `user_config.yaml.pre-migration-<timestamp>`
3. Deletes `~/.logos-node/data/` (chain DB + logs)
4. Rebuilds the Docker image
5. Regenerates `user_config.yaml` with fresh wallet keys
5. Regenerates `user_config.yaml`. If a `keystore.yaml` exists (any 0.2.0+ install), your wallet key identities are preserved via the node's `update-config` command; 0.1.2-era configs are converted with `migrate-from-0.1.2`, also preserving keys. Only a fully fresh install generates new keys.
6. Restarts the node (and monitoring, if it was running)

## After migration
Expand Down
69 changes: 46 additions & 23 deletions lib/cmd_reset.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,30 +49,42 @@ _perform_migration() {
log_warn "${BOLD}This will wipe local node data${RESET}"
fi

# Detect whether the current on-disk config predates 0.2.0 — if so, we
# can preserve wallet identities across the genesis reset by running
# `migrate-from-0.1.2` before wiping data. The stable discriminator is
# `tip_poll:` under cryptarchia.network.sync (0.2.0-only marker); its
# absence means the config is 0.1.x shape. Both 0.1.2 and 0.2.0 have
# top-level `wallet:` and `known_keys:` under it, so those don't work
# as version markers. Funds still don't carry over — only identities.
local preserve_keys=false
# Pick the config-regeneration strategy. Three cases:
#
# "migrate-012" — on-disk config predates 0.2.0 (no `tip_poll:` marker
# under cryptarchia.network.sync, the stable 0.2.x-only discriminator;
# top-level `wallet:` / `known_keys:` exist in both shapes so they
# don't work as markers). Run `migrate-from-0.1.2` to convert the
# config shape while preserving wallet identities.
# "update-config" — config is already 0.2.x shape and keystore.yaml
# exists (any 0.2.0+ install). Run `update-config` to regenerate the
# config from the existing keystore, keeping key identities stable.
# Also required mechanically: since 0.2.1 `init-config` refuses to
# run when keystore.yaml exists.
# "init-config" — no usable config/keystore. Generate everything fresh.
#
# Funds never carry over a genesis reset — only key identities do.
local strategy="init-config"
local config_path
config_path="$(get_user_config_path)"
local keystore_path
keystore_path="$(get_keystore_path)"
if [[ -f "$config_path" ]] && ! grep -qE '^[[:space:]]+tip_poll:[[:space:]]*$' "$config_path"; then
preserve_keys=true
strategy="migrate-012"
elif [[ -f "$keystore_path" ]]; then
strategy="update-config"
fi

log_info "Steps that will run:"
log_info " 1. Stop node (and monitoring, if running)"
log_info " 2. Back up ${BOLD}user_config.yaml${RESET} → ${BOLD}user_config.yaml.pre-migration-<timestamp>${RESET}"
log_info " 3. Delete ${BOLD}${LOGOS_NODE_DIR}/data/${RESET} (chain DB + logs)"
log_info " 4. Rebuild Docker image for the current node version"
if [[ "$preserve_keys" == "true" ]]; then
log_info " 5. Migrate 0.1.2 → 0.2.0 config ${BOLD}(preserves wallet keys)${RESET}"
else
log_info " 5. Regenerate fresh ${BOLD}user_config.yaml${RESET} (new wallet keys)"
fi
case "$strategy" in
migrate-012) log_info " 5. Migrate 0.1.2 → 0.2.x config ${BOLD}(preserves wallet keys)${RESET}" ;;
update-config) log_info " 5. Regenerate ${BOLD}user_config.yaml${RESET} from keystore ${BOLD}(preserves wallet keys)${RESET}" ;;
*) log_info " 5. Regenerate fresh ${BOLD}user_config.yaml${RESET} (new wallet keys)" ;;
esac
log_info " 6. Restart node (and monitoring, if it was running)"
echo ""
log_dim "After migration you must request faucet funds again — the new chain starts from zero."
Expand Down Expand Up @@ -101,20 +113,31 @@ _perform_migration() {
# cmd_start auto-restarts monitoring at the end if the compose file exists.

# ── Step 2: back up user_config.yaml ──────────────────────────────
# (config_path already resolved above during preserve_keys detection)
# (config_path already resolved above during strategy detection)
if [[ -f "$config_path" ]]; then
local backup_path="${config_path}.pre-migration-$(date +%Y%m%d-%H%M%S)"
cp "$config_path" "$backup_path"
chmod 600 "$backup_path"
log_success "Backed up config to ${BOLD}${backup_path}${RESET}"
# In the fresh-keys path we drop the old config so init-config generates
# cleanly. In the preserve-keys path we keep it in place because
# migrate-from-0.1.2 needs to read it.
if [[ "$preserve_keys" != "true" ]]; then
# cleanly. migrate-from-0.1.2 needs the old config in place to read it;
# update-config overwrites it in place (-y), so both preserve paths
# leave it where it is.
if [[ "$strategy" == "init-config" ]]; then
rm -f "$config_path"
fi
fi

# In the fresh-keys path a leftover keystore.yaml would make init-config
# hard-fail ("Keystore file exists" since 0.2.1) — back it up and clear it.
if [[ "$strategy" == "init-config" ]] && [[ -f "$keystore_path" ]]; then
local ks_backup="${keystore_path}.pre-migration-$(date +%Y%m%d-%H%M%S)"
cp "$keystore_path" "$ks_backup"
chmod 600 "$ks_backup"
rm -f "$keystore_path"
log_success "Backed up keystore to ${BOLD}${ks_backup}${RESET}"
fi

# ── Step 3: wipe data dir ─────────────────────────────────────────
log_step "Wiping ${LOGOS_NODE_DIR}/data/ ..."
rm -rf "${LOGOS_NODE_DIR}/data"
Expand All @@ -141,11 +164,11 @@ _perform_migration() {
fi

# ── Step 5: regenerate config ─────────────────────────────────────
if [[ "$preserve_keys" == "true" ]]; then
docker_migrate_from_012 || die "Failed to migrate 0.1.2 config to 0.2.0"
else
docker_init_config || die "Failed to regenerate node configuration"
fi
case "$strategy" in
migrate-012) docker_migrate_from_012 || die "Failed to migrate 0.1.2 config to 0.2.x" ;;
update-config) docker_update_config || die "Failed to regenerate config from keystore" ;;
*) docker_init_config || die "Failed to regenerate node configuration" ;;
esac

# ── Show new keys + faucet ────────────────────────────────────────
echo ""
Expand Down
12 changes: 8 additions & 4 deletions lib/cmd_start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,14 @@ _show_brief_status() {

if [[ -n "$consensus" ]]; then
local mode slot height
# 0.2.0 wraps mode in an enum object: "mode":{"Started":"Bootstrapping"}.
# 0.1.x returned a scalar: "mode":"Bootstrapping". Try wrapped first,
# fall back to scalar, else leave empty.
mode="$(echo "$consensus" | sed -nE 's/.*"mode":[[:space:]]*\{"[^"]+":[[:space:]]*"([^"]+)"\}.*/\1/p')"
# 0.2.1 renamed the field: "state":"Bootstrapping" (scalar). 0.2.0
# wrapped it in an enum object: "mode":{"Started":"Bootstrapping"}.
# 0.1.x was a scalar "mode":"Bootstrapping". Try newest shape first,
# fall back through the older ones, else leave empty.
mode="$(echo "$consensus" | sed -nE 's/.*"state":[[:space:]]*"([^"]+)".*/\1/p')"
if [[ -z "$mode" ]]; then
mode="$(echo "$consensus" | sed -nE 's/.*"mode":[[:space:]]*\{"[^"]+":[[:space:]]*"([^"]+)"\}.*/\1/p')"
fi
if [[ -z "$mode" ]]; then
mode="$(echo "$consensus" | sed -nE 's/.*"mode":[[:space:]]*"([^"]+)".*/\1/p')"
fi
Expand Down
31 changes: 19 additions & 12 deletions lib/cmd_status.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,25 @@ cmd_status() {
consensus="$(curl -sf "${api_url}/cryptarchia/info" 2>/dev/null)" || true

if [[ -n "$consensus" ]]; then
local mode slot height lib tip
# 0.2.0 wraps the mode in an enum object: "mode":{"Started":"Bootstrapping"}.
# 0.1.x returned a scalar: "mode":"Bootstrapping". Try the wrapped shape
# first (extracts the inner variant), fall back to the scalar. On no
# match the substitution leaves the whole body — detect and blank out.
mode="$(echo "$consensus" | sed -nE 's/.*"mode":[[:space:]]*\{"[^"]+":[[:space:]]*"([^"]+)"\}.*/\1/p')"
local mode phase slot height lib tip
# 0.2.1 renamed the field: "state":"Bootstrapping" (scalar) plus a
# top-level "phase" (e.g. "InitialBlockDownload"). 0.2.0 wrapped it in
# an enum object: "mode":{"Started":"Bootstrapping"}. 0.1.x was a
# scalar "mode":"Bootstrapping". Try newest shape first.
mode="$(echo "$consensus" | sed -nE 's/.*"state":[[:space:]]*"([^"]+)".*/\1/p')"
if [[ -z "$mode" ]]; then
mode="$(echo "$consensus" | sed -nE 's/.*"mode":[[:space:]]*\{"[^"]+":[[:space:]]*"([^"]+)"\}.*/\1/p')"
fi
if [[ -z "$mode" ]]; then
mode="$(echo "$consensus" | sed -nE 's/.*"mode":[[:space:]]*"([^"]+)".*/\1/p')"
fi
phase="$(echo "$consensus" | sed -nE 's/.*"phase":[[:space:]]*"([^"]+)".*/\1/p')"
slot="$(echo "$consensus" | sed -E 's/.*"slot":([0-9]+).*/\1/')"
height="$(echo "$consensus" | sed -E 's/.*"height":([0-9]+).*/\1/')"

case "$mode" in
Online) log_success "Mode: ${GREEN}${mode}${RESET}" ;;
Bootstrapping) log_info "Mode: ${YELLOW}${mode}${RESET} (syncing...)" ;;
Bootstrapping) log_info "Mode: ${YELLOW}${mode}${RESET} (syncing...${phase:+ phase: ${phase}})" ;;
"") log_info "Mode: ${DIM}(unknown)${RESET}" ;;
*) log_info "Mode: ${mode}" ;;
esac
Expand Down Expand Up @@ -197,12 +201,15 @@ _scan_recent_node_logs() {
window="$($DOCKER_CMD logs --since 60s "$LOGOS_CONTAINER_NAME" 2>&1)" || return 0
[[ -z "$window" ]] && return 0

# grep -c prints the count (including "0") on its own — it just also
# exits 1 when the count is 0, so the fallback must NOT echo a second
# "0" (that yields "0\n0" and breaks the (( )) checks below).
local proto_mismatch ibd_failed ntp_failed net_unreach no_gateway
proto_mismatch="$(echo "$window" | grep -cE 'does not support /logos-blockchain-[a-z]+-[0-9]+\.[0-9]+\.[0-9]+/chainsync' 2>/dev/null || echo 0)"
ibd_failed="$(echo "$window" | grep -cE 'Initial Block Download failed: AllPeersFailed' 2>/dev/null || echo 0)"
ntp_failed="$(echo "$window" | grep -cE 'NTP sync failed' 2>/dev/null || echo 0)"
net_unreach="$(echo "$window" | grep -cE 'Network is unreachable|Temporary failure in name resolution' 2>/dev/null || echo 0)"
no_gateway="$(echo "$window" | grep -cE 'Failed to detect gateway|Failed to get default gateway' 2>/dev/null || echo 0)"
proto_mismatch="$(echo "$window" | grep -cE 'does not support /logos-blockchain-[a-z]+-[0-9]+\.[0-9]+\.[0-9]+/chainsync' 2>/dev/null || true)"
ibd_failed="$(echo "$window" | grep -cE 'Initial Block Download failed: AllPeersFailed' 2>/dev/null || true)"
ntp_failed="$(echo "$window" | grep -cE 'NTP sync failed' 2>/dev/null || true)"
net_unreach="$(echo "$window" | grep -cE 'Network is unreachable|Temporary failure in name resolution' 2>/dev/null || true)"
no_gateway="$(echo "$window" | grep -cE 'Failed to detect gateway|Failed to get default gateway' 2>/dev/null || true)"

# Nothing to say → don't print the section header at all.
if (( proto_mismatch == 0 && ibd_failed == 0 && ntp_failed == 0 && net_unreach == 0 && no_gateway == 0 )); then
Expand Down
94 changes: 85 additions & 9 deletions lib/docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -179,16 +179,15 @@ docker_init_config() {

# Build init-config args. --http-host takes a full SocketAddr (host:port),
# not just a host; port 8080 matches the container-internal API port that
# the compose file forwards. --ibd populates
# cryptarchia.network.bootstrap.ibd.peers from the -p peer IDs so the
# node actually downloads historical blocks from bootstrap peers instead
# of waiting on gossip forever (gossip only carries new blocks; without
# IBD a fresh node hovers at height 0 with peers connected).
# the compose file forwards. Since 0.2.1 IBD is on by default: the -p peer
# IDs auto-populate cryptarchia.network.bootstrap.ibd.peers so the node
# downloads historical blocks from bootstrap peers instead of waiting on
# gossip forever (the old --ibd opt-in flag was replaced by --skip-ibd,
# which we never pass).
local init_args=(
--output /app/user_config.yaml
--keystore /app/keystore.yaml
--http-host 0.0.0.0:8080
--ibd
)

# Bootstrap peers as -p /ip4/.../p2p/... (comma-separated in LOGOS_BOOTSTRAP_PEERS)
Expand Down Expand Up @@ -303,15 +302,15 @@ docker_migrate_from_012() {
# here (the release notes' quick-start relies on this too). Without
# this, migrated nodes come up with zero fleet contacts and only find
# peers through leftover DHT cache, which almost never surfaces the
# 0.2.0 fleet. Reuse the same LOGOS_BOOTSTRAP_PEERS the fresh-install
# path uses.
# current fleet. Reuse the same LOGOS_BOOTSTRAP_PEERS the fresh-install
# path uses. IBD from those peers is on by default since 0.2.1 (the old
# --ibd opt-in flag was replaced by --skip-ibd, which we never pass).
local migrate_args=(
migrate-from-0.1.2
--old-config /app/user_config.yaml
--new-config /app/user_config.migrated.yaml
--keystore /app/keystore.yaml
--http-host 0.0.0.0:8080
--ibd
)
local _p
IFS=',' read -ra _peers <<< "$LOGOS_BOOTSTRAP_PEERS"
Expand Down Expand Up @@ -355,6 +354,83 @@ docker_migrate_from_012() {
return 0
}

# Regenerate user_config.yaml from an EXISTING keystore.yaml via the node's
# `update-config` subcommand. This is the migration path for 0.2.x → 0.2.1+
# breaking releases: the config shape is already modern, but the chain was
# reset, so we want a freshly generated config (new defaults, new fields)
# while keeping the operator's key identities stable. Funds never survive a
# genesis reset, but stable public keys mean dashboards, faucet history and
# any allowlists keep pointing at the same operator.
#
# Also load-bearing for a subtler reason: since 0.2.1, `init-config` hard-fails
# when keystore.yaml already exists ("Keystore file exists. Use `update`
# command."), so the old wipe-and-init flow cannot work for operators coming
# from 0.2.0 — this is the path the node itself directs us to.
docker_update_config() {
local config_path
config_path="$(get_user_config_path)"
local keystore_path
keystore_path="$(get_keystore_path)"

[[ -f "$keystore_path" ]] || {
log_error "No keystore.yaml found — cannot run update-config"
return 1
}

local update_args=(
update-config
-y
--user-config /app/user_config.yaml
--keystore /app/keystore.yaml
--http-host 0.0.0.0:8080
)

local _p
IFS=',' read -ra _peers <<< "$LOGOS_BOOTSTRAP_PEERS"
for _p in "${_peers[@]}"; do
update_args+=("-p" "$_p")
done
if [[ -n "${LOGOS_EXTERNAL_IP:-}" ]]; then
update_args+=("--external-address" "/ip4/${LOGOS_EXTERNAL_IP}/udp/${LOGOS_UDP_PORT}/quic-v1")
log_dim "Advertising external address: /ip4/${LOGOS_EXTERNAL_IP}/udp/${LOGOS_UDP_PORT}/quic-v1"
fi

log_step "Regenerating node configuration from existing keystore..."
log_dim "Running logos-blockchain-node update-config (wallet keys preserved)"

local host_uid
host_uid="$(id -u)"
local host_gid
host_gid="$(id -g)"

mkdir -p "${LOGOS_NODE_DIR}/data"

$DOCKER_CMD run --rm \
--user "${host_uid}:${host_gid}" \
-v "${LOGOS_NODE_DIR}:/app" \
-w /app \
"${LOGOS_DOCKER_IMAGE}:${LOGOS_NODE_VERSION}" \
"${update_args[@]}" 2>&1 | while IFS= read -r line; do
echo -e " ${DIM}${line}${RESET}"
done

if [[ ! -f "$config_path" ]]; then
log_error "update-config did not produce a new config"
return 1
fi

chmod 600 "$config_path"
chmod 600 "$keystore_path"

# Same compose-friendly patches as the init-config path (all idempotent).
patch_user_config_for_http_bind "$config_path"
patch_user_config_for_otlp "$config_path"
patch_user_config_for_log_files "$config_path"

log_success "Regenerated $config_path; wallet keys preserved in $keystore_path"
return 0
}

# Rewrite api.backend.listen_address so the node binds 0.0.0.0 inside the
# container. Docker's port map forwards host:8080 to container_ip:8080 — if
# the node listens on 127.0.0.1:8080 inside the container, the forward can't
Expand Down
2 changes: 1 addition & 1 deletion lib/releases.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Versions that introduce breaking on-chain changes (genesis reset, incompatible
# state). Updating across one of these requires wiping local data and
# regenerating user_config.yaml. Add new entries here as the chain evolves.
LOGOS_BREAKING_VERSIONS=("0.1.2" "0.2.0")
LOGOS_BREAKING_VERSIONS=("0.1.2" "0.2.0" "0.2.1")

is_breaking_version() {
local v="${1#v}"
Expand Down
9 changes: 6 additions & 3 deletions monitoring/exporter/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,12 @@ def poll_node_api():

node_up.set(1)

# 0.2.0: mode is an enum object like {"Started": "Bootstrapping"}
# 0.1.x: mode was a scalar string like "Bootstrapping"
mode_field = data.get("mode", "Unknown")
# 0.2.1: scalar "state" inside "cryptarchia_info" (e.g. "Bootstrapping")
# 0.2.0: "mode" is an enum object like {"Started": "Bootstrapping"}
# 0.1.x: "mode" was a scalar string like "Bootstrapping"
mode_field = data.get("cryptarchia_info", {}).get("state") or data.get(
"mode", "Unknown"
)
if isinstance(mode_field, dict) and mode_field:
mode = next(iter(mode_field.values()), "Unknown")
else:
Expand Down
3 changes: 2 additions & 1 deletion network.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ network: testnet
# Once connected, the node uses Kademlia to find additional peers.
bootstrap_peers:
# Updated for 0.2.0 release (genesis reset rotated peer IDs).
# Source: https://github.com/logos-blockchain/logos-blockchain/releases/tag/0.2.0
# Verified unchanged for 0.2.1 (genesis reset again, but same fleet peers).
# Source: https://github.com/logos-blockchain/logos-blockchain/releases/tag/0.2.1
- /ip4/65.109.51.37/udp/3000/quic-v1/p2p/12D3KooWFrouXfmrR4nsLMtE7wu15DoMJ6VtoUtHinREZCvbWHar
- /ip4/65.109.51.37/udp/3001/quic-v1/p2p/12D3KooWJRGau8M1rjT7R5e4YYsgdFhsMX35nRDtMwCDjxQkXAHz
- /ip4/65.109.51.37/udp/3002/quic-v1/p2p/12D3KooWQXJavMDTRscjauFSgVAB1VLB6Rzpy2uY5SU9Tk7927tb
Expand Down
2 changes: 1 addition & 1 deletion settings.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

# ── Optional: public IP hint ────────────────────────────────────────────
# Set to your node's public IPv4 to advertise it directly to peers and skip
# NAT traversal. Applied when init-config / migrate-from-0.1.2 runs.
# NAT traversal. Applied when init-config / update-config / migrate-from-0.1.2 runs.
# LOGOS_EXTERNAL_IP=203.0.113.42

# ── Bootstrap peers ─────────────────────────────────────────────────────
Expand Down