/* Copyright (c) 2009-2010 Satoshi Nakamoto
 * Copyright (c) 2009-2014 The Bitcoin Core developers
 * Copyright 2026 Rhett Creighton - Apache License 2.0
 * Distributed under the MIT software license, see the accompanying
 * file COPYING or http://www.opensource.org/licenses/mit-license.php. */

#include "platform/time_compat.h"
#include "config/boot_internal.h"
#include "config/boot_snapshot_import.h"
#include "config/file_ops.h"
#include "platform/rng.h"
#include "net/snapshot_sync_contract.h"
#include "services/chain_activation_service.h"
#include "services/chain_restore_boot_snapshot.h"
#include "services/chain_restore_executor.h"
#include "services/chain_restore_integrity.h"
#include "services/chain_restore_repair.h"
#include "services/chain_state_service.h"
#include "util/service_state.h"
#include "services/service_state_driver.h"
#include "services/chain_tip.h"
#include "services/recovery_policy.h"
#include "services/utxo_recovery_service.h"
#include "storage/progress_store.h"
#include "jobs/reducer_frontier.h"
#include "jobs/stage_repair.h"
#include "storage/utxo_reimport_flag.h"
#include "config/boot_crashonly.h"
#include "services/header_probe.h"
#include "services/block_index_integrity.h"
#include "services/wallet_backup_service.h"
#include "services/disk_monitor.h"
#include "services/ibd_throttle.h"
#include "services/db_maintenance.h"
#include "sim/postmortem.h"
#include "sim/seed_tape.h"
#include "controllers/wallet_scan.h"
#include "util/blocker.h"
#include "util/signal_handler.h"
#include "util/sync.h"
#include "util/safe_alloc.h"
#include "util/boot_phase.h"
#include "util/util.h"
#include "net/msgprocessor.h"
#include "chain/chainparams.h"
#include "keys/key.h"
#include "keys/pubkey.h"
#include "coins/coins_view.h"
#include "coins/utxo_commitment.h"
#include "chain/checkpoints.h"
#include "storage/coins_view_sqlite.h"
#include "storage/coins_view_kv.h"
#include "storage/coins_kv.h"
#include "storage/utxo_projection.h"
#include "storage/coins_db.h"
#include "storage/ldb_snapshot.h"
#include "consensus/validation.h"
#include "controllers/blockchain_controller.h"
#include "controllers/repair_controller.h"
#include "controllers/chain_inspect_controller.h"
#include "controllers/misc_controller.h"
#include "controllers/network_controller.h"
#include "rpc/httpserver.h"
#include "controllers/mining_controller.h"
#include "controllers/transaction_controller.h"
#include "rpc/server.h"
#include "storage/block_index_db.h"
#include "storage/block_index_projection.h"
#include "storage/disk_block_io.h"
#include "services/block_index_loader.h"
#include "validation/chainstate.h"
#include "validation/main_state.h"
#include "validation/process_block.h"
#include "validation/contextual_check_tx.h"
#include "net/connman.h"
#include "keys/key_io.h"
#include "mining/gen.h"
#include "script/standard.h"
#include "controllers/api_controller.h"
#include "controllers/explorer_internal.h"
#include "controllers/explorer_controller.h"
#include "controllers/wallet_controller.h"
#include "controllers/zslp_controller.h"
#include "wallet/wallet.h"
#include "wallet/wallet_sqlite.h"
#include "wallet/wallet_canary.h"
#include "wallet/wallet_db.h"
#include "sapling/params_init.h"
#include "metrics/metrics.h"
#include "chain/pow.h"
#include "controllers/sync_controller.h"
#include "controllers/legacy_import.h"
#include "controllers/snapshot_controller.h"
#include "validation/update_coins.h"
#include "validation/connect_block.h"
/* LevelDB dbwrapper only used for legacy import paths */
#include "net/tor_integration.h"
#include "net/https_server.h"
#include "net/fast_sync.h"
#include "net/peer_strategy.h"
#include "event/event.h"
#include "controllers/event_controller.h"
#include "models/block.h"
#include <netdb.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <signal.h>
#include <time.h>
#include <pthread.h>
#include <malloc.h>
#include <errno.h>
#include <limits.h>
#include <sys/sysinfo.h>
#include <sqlite3.h>

static struct main_state g_state;
static struct coins_view_sqlite g_coins_sqlite;
/* Read authority for the coins_tip cache backing: a read-only coins_view over
 * coins_kv (canonical UTXO set in progress.kv, authored in-txn by the reducer —
 * atomically consistent with the stage cursor on every crash). The legacy
 * g_coins_sqlite is still opened for legacy damage-recovery best-block reads
 * below; retiring that fallback is remaining one-write-path debt. */
static struct coins_view_kv g_coins_read_view;
static struct coins_view_cache g_coins_tip;
static struct chain_activation_controller g_activation_ctl;

struct chain_activation_controller *boot_activation_controller(void)
{
    return &g_activation_ctl;
}
static struct block_tree_db g_block_tree;
struct block_tree_db *g_active_block_tree = NULL;
static bool g_block_tree_open = false;
static struct tx_mempool g_mempool;
static struct rpc_table g_rpc_table;
static struct msg_processor g_msg_processor;
static struct connman g_connman;
static struct wallet g_wallet;
static struct gen_context g_gen;
static struct wallet_sqlite g_wallet_sqlite;
static struct node_db g_node_db;
static struct db_service g_db_service;
static struct app_runtime_context g_boot_runtime;
static const char *g_datadir = NULL;
const char *g_blog_datadir = NULL;
static _Atomic bool g_running = false;
static struct wallet_backup_config g_wallet_backup_cfg;
static struct disk_monitor_config g_disk_monitor_cfg;
static struct ibd_throttle_config g_ibd_throttle_cfg;
static struct zcl_service_kernel g_guard_kernel;
static struct zcl_service_kernel g_maintenance_kernel;
static struct zcl_service_kernel g_boot_db_kernel;
static seed_tape_t *g_boot_seed_tape = NULL;
static char g_boot_postmortem_dir[1024];

#define BOOT_POSTMORTEM_MAX_AGE_SECONDS (30LL * 24LL * 60LL * 60LL)
#define BOOT_POSTMORTEM_KEEP_LATEST 100u
#define BOOT_POSTMORTEM_EVENT_TYPE 1u
#define BOOT_POSTMORTEM_EVENT_PAYLOAD "boot-postmortem-installed"

/* ── System RAM query ────────────────────────────────────────── */

static size_t get_system_ram(void)
{
    struct sysinfo si;
    if (sysinfo(&si) != 0)
        return 0;
    return (size_t)si.totalram * (size_t)si.mem_unit;
}

/* ── PID lock file for data directory ─────────────────────────── */

static char g_pidfile_path[1024];

/* Acquire data directory lock. Returns true if lock acquired,
 * false if another instance is running. */
static bool acquire_datadir_lock(const char *datadir)
{
    snprintf(g_pidfile_path, sizeof(g_pidfile_path), "%s/zclassic23.pid",
             datadir);

    /* Check existing PID file */
    FILE *f = fopen(g_pidfile_path, "r");
    if (f) {
        char buf[32] = {0};
        size_t n = fread(buf, 1, sizeof(buf) - 1, f);
        fclose(f);
        if (n > 0) {
            long old_pid = strtol(buf, NULL, 10);
            if (old_pid > 0) {
                if (kill((pid_t)old_pid, 0) == 0) {
                    fprintf(stderr,
                        "[boot] Data directory locked by PID %ld (running). "
                        "Cannot start.\n", old_pid);
                    return false;
                }
                printf("[boot] Stale lock detected (PID %ld is not running). "
                       "Removing lock file.\n", old_pid);
            }
        }
    }

    /* Write our PID */
    f = fopen(g_pidfile_path, "w");
    if (!f) {
        fprintf(stderr, "[boot] Cannot create PID file %s: %s\n",
                g_pidfile_path, strerror(errno));
        return true; /* non-fatal — proceed without lock */
    }
    fprintf(f, "%ld\n", (long)getpid());
    fclose(f);
    return true;
}

static void release_datadir_lock(void)
{
    if (g_pidfile_path[0])
        unlink(g_pidfile_path);
}

static void boot_step_stop_postmortem(void)
{
    postmortem_uninstall();
    seed_tape_t *tape = g_boot_seed_tape;
    g_boot_seed_tape = NULL;
    g_boot_postmortem_dir[0] = '\0';
    if (tape)
        seed_tape_close(tape);
}

static bool boot_step_init_postmortem(const char *datadir)
{
    if (!datadir || !*datadir)
        return false;

    if (g_boot_seed_tape)
        boot_step_stop_postmortem();

    int n = snprintf(g_boot_postmortem_dir, sizeof(g_boot_postmortem_dir),
                     "%s/postmortems", datadir);
    if (n < 0 || (size_t)n >= sizeof(g_boot_postmortem_dir)) {
        fprintf(stderr,  // obs-ok:boot-fatal-before-event-context
                "WARNING: postmortem path too long; crash capsules disabled\n");
        g_boot_postmortem_dir[0] = '\0';
        return false;
    }

    uint64_t seed = rng_u64();
    int64_t wall = platform_time_wall_time_t();
    seed_tape_t *tape = seed_tape_open(seed, wall);
    if (!tape) {
        fprintf(stderr,  // obs-ok:boot-fatal-before-event-context
                "WARNING: seed_tape_open failed; crash capsules disabled\n");
        return false;
    }
    (void)seed_tape_inject(tape, BOOT_POSTMORTEM_EVENT_TYPE,
                           BOOT_POSTMORTEM_EVENT_PAYLOAD,
                           strlen(BOOT_POSTMORTEM_EVENT_PAYLOAD));

    int rc = postmortem_install(tape, g_boot_postmortem_dir);
    if (rc != 0) {
        fprintf(stderr,  // obs-ok:boot-fatal-before-event-context
                "WARNING: postmortem_install failed rc=%d; crash capsules disabled\n",
                rc);
        seed_tape_close(tape);
        g_boot_postmortem_dir[0] = '\0';
        return false;
    }

    g_boot_seed_tape = tape;
    size_t compressed = 0;
    rc = postmortem_capsule_compress_unpacked(g_boot_postmortem_dir,
                                              &compressed);
    if (rc != 0) {
        fprintf(stderr,  // obs-ok:boot-fatal-before-event-context
                "WARNING: postmortem compression failed rc=%d\n", rc);
    } else if (compressed > 0) {
        printf("[boot] postmortem compressed %zu capsule(s)\n", compressed);
    }

    size_t pruned = 0;
    rc = postmortem_capsule_prune(g_boot_postmortem_dir,
                                  platform_time_wall_time_t(),
                                  BOOT_POSTMORTEM_MAX_AGE_SECONDS,
                                  BOOT_POSTMORTEM_KEEP_LATEST,
                                  &pruned);
    if (rc != 0) {
        fprintf(stderr,  // obs-ok:boot-fatal-before-event-context
                "WARNING: postmortem prune failed rc=%d\n", rc);
    } else if (pruned > 0) {
        printf("[boot] postmortem pruned %zu old capsule(s)\n", pruned);
    }
    printf("[boot] postmortem capsules: %s\n", g_boot_postmortem_dir);
    return true;
}

#ifdef ZCL_TESTING
bool boot_postmortem_init_for_testing(const char *datadir)
{
    return boot_step_init_postmortem(datadir);
}

void boot_postmortem_shutdown_for_testing(void)
{
    boot_step_stop_postmortem();
}

const char *boot_postmortem_dir_for_testing(void)
{
    return g_boot_postmortem_dir[0] ? g_boot_postmortem_dir : NULL;
}
#endif

static struct db_service *boot_runtime_db_service(void)
{
    return db_service_is_started(&g_db_service) ? &g_db_service : NULL;
}

/* Policy-gated UTXO wipe moved to utxo_recovery_service.c.
 * boot_policy_wipe_utxos → utxo_recovery_wipe(&g_node_db, reason) */

static bool boot_db_enter_turbo_mode(void)
{
    struct db_service *dbsvc = boot_runtime_db_service();

    if (dbsvc)
        return db_service_ibd_turbo_mode(dbsvc);
    return g_node_db.open && node_db_ibd_turbo_mode(&g_node_db);
}

static bool boot_db_restore_normal_mode(void)
{
    struct db_service *dbsvc = boot_runtime_db_service();

    if (dbsvc)
        return db_service_normal_mode(dbsvc);
    return g_node_db.open && node_db_normal_mode(&g_node_db);
}

static bool boot_db_set_sync_batch_size(int batch_size)
{
    struct db_service *dbsvc = boot_runtime_db_service();

    if (dbsvc)
        return db_service_set_sync_batch_size(dbsvc, batch_size);
    if (!g_node_db.open)
        return false;
    node_db_set_sync_batch_size(&g_node_db, batch_size);
    return true;
}

/* Shielded backfill moved to utxo_recovery_service.c —
 * utxo_recovery_backfill_shielded(). */
static struct metrics_context g_metrics;

/* Comparator for sorting block_index pointers by height (for qsort). */
static int cmp_block_index_height(const void *a, const void *b)
{
    const struct block_index *pa = *(const struct block_index **)a;
    const struct block_index *pb = *(const struct block_index **)b;
    return (pa->nHeight > pb->nHeight) - (pa->nHeight < pb->nHeight);
}

/* Callback for block_tree_db_load_block_index_guts — inserts a block
 * into the block map, reusing existing entry if hash already present. */
static struct block_index *boot_insert_block_index_cb(void *ctx_ptr,
                                                       const struct uint256 *hash)
{
    struct main_state *ms = (struct main_state *)ctx_ptr;
    return chainstate_insert_block_index((struct chainstate *)ms, hash);
}

/* SQLite tuning and file operations now live in the model layer:
 *   node_db_ibd_turbo_mode()  — database.h
 *   node_db_normal_mode()     — database.h
 *   file_copy(), dir_copy()   — file_ops.h
 */

/* Background ZK param loading */
static char g_params_dir_buf[1024];
static pthread_t g_params_thread;
static bool g_params_thread_started = false;
static _Atomic bool g_params_loaded = false;
static struct boot_svc_ctx g_svc;

/* Single source of truth for the live boot service context — the &g_svc handed
 * to app_init_services; boot_services.c's main.c-facing entry points read it. */
struct boot_svc_ctx *boot_active_svc(void) { return &g_svc; }

void *load_params_thread(void *arg)
{
    (void)arg;
    printf("Loading verification keys (background)...\n");
    if (sapling_init_params(g_params_dir_buf))
        atomic_store(&g_params_loaded, true);
    else
        fprintf(stderr, "Warning: Failed to load ZK params\n");
    printf("Verification keys loaded.\n");
    return NULL;
}

/* Block index, chainstate rebuild, and address backfill are in
 * boot_index.c. Service startup (P2P, RPC, Tor) and shutdown
 * are in boot_services.c. */

void app_context_defaults(struct app_context *ctx)
{
    memset(ctx, 0, sizeof(*ctx));
    ctx->datadir = NULL;
    ctx->params_dir = NULL;
    ctx->rpc_port = 18232;
    ctx->p2p_port = 8033;
    ctx->https_port = 8443;
    ctx->fs_port = 18034;
    ctx->listen = true;   /* accept inbound by default — be a good peer */
    ctx->checkpoints_enabled = true;
    ctx->runtime_profile = ZCL_RUNTIME_FULL;
}

const char *app_runtime_profile_name(enum zcl_runtime_profile profile)
{
    switch (profile) {
    case ZCL_RUNTIME_FULL:
        return "full";
    case ZCL_RUNTIME_ZCLASSIC_ONLY:
        return "zclassic-only";
    case ZCL_RUNTIME_EXPLORER:
        return "explorer";
    case ZCL_RUNTIME_ONION_NODE:
        return "onion-node";
    case ZCL_RUNTIME_LEGACY_COMPAT:
        return "legacy-compat";
    default:
        return "unknown";
    }
}

bool app_runtime_profile_parse(const char *name,
                               enum zcl_runtime_profile *out)
{
    if (!name || !out)
        return false;
    if (strcmp(name, "full") == 0) {
        *out = ZCL_RUNTIME_FULL;
        return true;
    }
    if (strcmp(name, "zclassic-only") == 0 ||
        strcmp(name, "zclassic") == 0) {
        *out = ZCL_RUNTIME_ZCLASSIC_ONLY;
        return true;
    }
    if (strcmp(name, "explorer") == 0) {
        *out = ZCL_RUNTIME_EXPLORER;
        return true;
    }
    if (strcmp(name, "onion-node") == 0 ||
        strcmp(name, "onion") == 0) {
        *out = ZCL_RUNTIME_ONION_NODE;
        return true;
    }
    if (strcmp(name, "legacy-compat") == 0 ||
        strcmp(name, "legacy") == 0) {
        *out = ZCL_RUNTIME_LEGACY_COMPAT;
        return true;
    }
    return false;
}

bool app_runtime_profile_has_explorer(enum zcl_runtime_profile profile)
{
    return profile == ZCL_RUNTIME_FULL ||
           profile == ZCL_RUNTIME_EXPLORER ||
           profile == ZCL_RUNTIME_ONION_NODE ||
           profile == ZCL_RUNTIME_LEGACY_COMPAT;
}

bool app_runtime_profile_has_store(enum zcl_runtime_profile profile)
{
    return profile == ZCL_RUNTIME_FULL ||
           profile == ZCL_RUNTIME_ONION_NODE ||
           profile == ZCL_RUNTIME_LEGACY_COMPAT;
}

bool app_runtime_profile_has_onion(enum zcl_runtime_profile profile,
                                   bool tor_flag)
{
    return tor_flag || profile == ZCL_RUNTIME_ONION_NODE;
}

bool app_runtime_profile_has_file_service(enum zcl_runtime_profile profile)
{
    return profile == ZCL_RUNTIME_FULL ||
           profile == ZCL_RUNTIME_ONION_NODE ||
           profile == ZCL_RUNTIME_LEGACY_COMPAT;
}

/* Boot timing helper */
static int64_t boot_clock_ms(void)
{
    struct timespec ts;
    platform_time_monotonic_timespec(&ts);
    return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}

/* ── boot.c decomposition ──────────────────────────────────────────
 * app_init is split into named, single-responsibility `boot_step_*`
 * static functions returning bool — true to continue, false to bail.
 * Contract: either the precondition holds and the step succeeds, or
 * the process exits non-zero before any other subsystem can observe
 * partial state. */

static bool boot_step_init_observability(void)
{
    /* Install fatal-signal handler BEFORE any thread is spawned so the
     * handler is inherited process-wide. Any SIGABRT/SIGSEGV/SIGBUS/SIGFPE
     * gets a logged backtrace before systemd sees the exit. */
    if (signal_handler_install() != 0) {
        /* Fatal: booting without a crash handler means the next SEGV/ABRT is
         * silent and undiagnosable — the exact failure mode that scrambled
         * block storage on 2026-05-30. Refuse to start blind. */
        fprintf(stderr, "FATAL: signal_handler_install failed; refusing to "
                        "boot without a crash handler\n");
        return false;
    }

    db_service_init(&g_db_service);

    /* Initialize event log first — everything after this is observable */
    event_log_init();
    event_install_crash_handler();

    /* Start async observer thread + register error accumulator.
     * Captures DB errors, block rejections, flush failures for
     * instant health queries via /api/health and healthcheck RPC. */
    if (!event_async_start())
        fprintf(stderr, "WARNING: failed to start async event dispatcher\n");
    struct error_ring *er = error_ring_global();
    event_observe_async(EV_DB_ERROR, error_ring_observer, er);
    event_observe_async(EV_COINS_FLUSH_FAILED, error_ring_observer, er);
    event_observe_async(EV_BLOCK_REJECTED, error_ring_observer, er);
    event_observe_async(EV_UTXO_CHECKPOINT_FAIL, error_ring_observer, er);

    /* Observe validation failures as errors too */
    event_observe_async(EV_MODEL_VALIDATION_FAILED, error_ring_observer, er);

    /* Typed blocker registry: init BEFORE the restore/import producers.
     * blocker_module_init memsets the registry, and its previous first
     * call sat in boot_services (AFTER restore), silently wiping any
     * band/producer blockers recorded during restore. Idempotent — the
     * boot_services call remains as a backstop. */
    blocker_module_init();

    event_emitf(EV_NODE_STARTING, 0, "zclassic23 1.0.0");
    return true;
}

static bool boot_step_select_chain_and_datadir(struct app_context *ctx)
{
    if (ctx->regtest)
        chain_params_select(CHAIN_REGTEST);
    else if (ctx->testnet)
        chain_params_select(CHAIN_TESTNET);
    else
        chain_params_select(CHAIN_MAIN);

    g_datadir = ctx->datadir;
    g_blog_datadir = ctx->datadir;
    SetDataDir(ctx->datadir);

    /* Auto-create datadir if it doesn't exist */
    struct stat st;
    if (stat(ctx->datadir, &st) != 0) {
        mkdir(ctx->datadir, 0700);
        printf("Created data directory: %s\n", ctx->datadir);
    }

    /* Now that the datadir is known, point the crash handler at a durable,
     * fsync'd crash log there. Until this call a crash still lands on stderr;
     * after it, the backtrace also survives in $datadir/crash_log.txt
     * independent of systemd's stderr routing. */
    {
        char crash_path[600];
        snprintf(crash_path, sizeof(crash_path), "%s/crash_log.txt",
                 ctx->datadir);
        signal_handler_set_crash_log(crash_path);
    }

    /* Acquire data directory lock — prevents two instances from
     * corrupting SQLite / LevelDB by writing concurrently. */
    if (!acquire_datadir_lock(ctx->datadir))
        return false;

    boot_stage_advance_to(BOOT_STAGE_DATADIR_LOCKED);
    return true;
}

static void boot_step_detect_unclean_shutdown(const char *datadir)
{
    /* A clean shutdown writes .shutdown_clean marker. If it's missing
     * and a WAL file exists, the previous run crashed. We don't fail
     * boot here — we surface the condition as an event for ops, then
     * unlink the marker so the next clean shutdown can rewrite it. */
    char marker_path[1024], wal_path[1024];
    snprintf(marker_path, sizeof(marker_path), "%s/.shutdown_clean", datadir);
    snprintf(wal_path, sizeof(wal_path), "%s/node.db-wal", datadir);

    struct stat wal_st;
    bool wal_exists = (stat(wal_path, &wal_st) == 0 && wal_st.st_size > 0);
    bool marker_exists = (access(marker_path, F_OK) == 0);

    if (!marker_exists && wal_exists) {
        printf("[boot] Unclean shutdown detected (WAL=%lldB, "
               "clean marker missing)\n",
               (long long)wal_st.st_size);
        event_emitf(EV_CRASH_RECOVERY_START, 0,
            "wal_size=%lld clean_marker=missing",
            (long long)wal_st.st_size);
    } else if (!marker_exists) {
        printf("[boot] First boot or marker absent (no WAL)\n");
    } else {
        printf("[boot] Clean shutdown marker present\n");
    }

    /* Remove marker at boot — will be re-created on clean shutdown */
    unlink(marker_path);
}

static void boot_step_backfill_shielded_if_needed(struct app_context *ctx,
                                                   struct block_index *tip)
{
    /* When the node has been running long enough to have a non-trivial
     * chain (tip > 100k blocks) but the blocks table is missing
     * sprout/sapling totals (< 1k rows with non-zero shielded value),
     * backfill via utxo_recovery_service. This catches the case where
     * an LDB import populated UTXOs but not the per-block shielded
     * column. Skipped in no_services mode (speedrun / benchmarking). */
    if (!(g_node_db.open && tip && tip->nHeight > 1000 && !ctx->no_services))
        return;

    int64_t shielded_count = 0;
    sqlite3_stmt *s = NULL;
    if (sqlite3_prepare_v2(g_node_db.db,
            "SELECT COUNT(*) FROM blocks WHERE sprout_value != 0 OR sapling_value != 0",
            -1, &s, NULL) == SQLITE_OK) {
        if (sqlite3_step(s) == SQLITE_ROW)
            shielded_count = sqlite3_column_int64(s, 0);
        sqlite3_finalize(s);
    }
    if (shielded_count < 1000 && tip->nHeight > 100000 &&
        !utxo_recovery_backfill_shielded(&g_node_db, boot_runtime_db_service(), &g_state, g_datadir, NULL).ok)
        LOG_WARN("boot", "shielded backfill: persistence/validation failure (see service log)");
}

static void boot_step_build_svc_ctx(struct app_context *ctx,
                                     struct boot_svc_ctx *svc)
{
    *svc = (struct boot_svc_ctx){
        .state = &g_state,
        .coins_sqlite = &g_coins_sqlite,
        .coins_tip = &g_coins_tip,
        .mempool = &g_mempool,
        .rpc_table = &g_rpc_table,
        .msg_processor = &g_msg_processor,
        .connman = &g_connman,
        .wallet = &g_wallet,
        .gen = &g_gen,
        .wallet_sqlite = &g_wallet_sqlite,
        .node_db = &g_node_db,
        .db_service = &g_db_service,
        .metrics = &g_metrics,
        .running = &g_running,
        .datadir = g_datadir,
        .params_thread = g_params_thread,
        .params_thread_started = g_params_thread_started,
        .params_loaded = &g_params_loaded,
        .block_tree_open = g_block_tree_open,
        .block_tree = &g_block_tree,
        .want_address_backfill = false,
        .want_snapshot_tx_index = ctx->tx_index || ctx->snapshot_dir != NULL,
        .defer_payment_service = false,
        .defer_offer_service = false,
    };
    if (g_node_db.open) {
        int64_t addr_done = 0;
        node_db_state_get_int(&g_node_db, "addresses_backfilled", &addr_done);
        svc->want_address_backfill = (addr_done == 0);
    }
}

static void boot_step_finalize_chain_state(void)
{
    /* Restore normal SQLite settings after any IBD replay */
    if (g_node_db.open) {
        if (!boot_db_restore_normal_mode())
            fprintf(stderr, "boot: failed to restore normal mode\n");
        if (!boot_db_set_sync_batch_size(1))
            fprintf(stderr, "boot: failed to reset sync batch size\n");
    }
    /* Flush every 500 blocks during normal sync so crash/kill never
     * loses more than ~500 blocks of connected coins state. */
    set_flush_policy(3600, 500000, 500);

    struct block_index *tip = active_chain_tip(&g_state.chain_active);
    if (tip && tip->phashBlock) {
        if (g_node_db.open &&
            !node_db_sync_set_tip(&g_node_db, tip->phashBlock->data,
                                  tip->nHeight)) {
            fprintf(stderr, "boot: failed to persist final chain tip\n");
        }
        char hex[65];
        uint256_get_hex(tip->phashBlock, hex);
        printf("Chain tip: height=%d hash=%s\n", tip->nHeight, hex);
        event_emitf(EV_BOOT_ACTIVATE, 0, "done tip=%d", tip->nHeight);

        /* Do not auto-extend deferred proof validation at startup.
         * Everything already in the chainstate was observed from either:
         * - zclassicd (via legacy import)
         * - a previous run of zclassic23
         * - the coins DB (hash-checked LevelDB state)
         * Only blocks received via P2P AFTER startup need new validation.
         * With Groth16 pairing fix deployed, all proofs above the checkpoint
         * are now verified correctly — no need to extend deferred proof validation. */
    } else {
        printf("Chain tip: genesis\n");
    }
}

bool boot_promote_tip_via_csr(struct block_index *tip,
                              const char *reason,
                              bool persist_coins_best)
{
    if (!tip || !tip->phashBlock)
        return false;

    struct chain_state_rollback_authorization rollback_auth = {
        .source = CSR_ROLLBACK_SOURCE_BOOT_REPAIR,
        .decision = POLICY_ALLOW,
        .from_height = active_chain_height(&g_state.chain_active),
        .to_height = tip->nHeight,
        .max_depth = INT64_MAX,
        .evidence_class = "boot_repair_verified",
        .reason = reason ? reason : "boot_repair",
    };
    struct chain_state_commit commit = {
        .new_tip = tip,
        .new_coins_best = *tip->phashBlock,
        .expected_utxo_count = 0,
        .update_header_tip = true,
        .persist_coins_best = persist_coins_best,
        .rollback_auth = &rollback_auth,
        .wallet_scan_height = -1,
        .reason = reason ? reason : "boot_repair",
    };

    enum csr_result rc = csr_commit_tip(csr_instance(), &commit);
    if (rc != CSR_OK) {
        fprintf(stderr, // obs-ok:pre-existing-diagnostic
                "[boot] csr rejected boot repair promotion (%s) "
                "reason=%s h=%d\n",
                csr_result_name(rc), reason ? reason : "", tip->nHeight);
        return false;
    }

    return true;
}

static bool boot_promote_header_via_csr(struct block_index *header,
                                        const char *reason)
{
    if (!header || !header->phashBlock)
        return false;

    struct chain_state_rollback_authorization rollback_auth = {
        .source = CSR_ROLLBACK_SOURCE_BOOT_REPAIR,
        .decision = POLICY_ALLOW,
        .from_height = g_state.pindex_best_header
            ? g_state.pindex_best_header->nHeight : -1,
        .to_height = header->nHeight,
        .max_depth = INT64_MAX,
        .evidence_class = "boot_header_verified",
        .reason = reason ? reason : "boot_header",
    };
    struct chain_state_header_commit commit = {
        .new_header_tip = header,
        .rollback_auth = &rollback_auth,
        .reason = reason ? reason : "boot_header",
    };
    enum csr_result rc = csr_commit_header_tip(csr_instance(), &commit);
    if (rc != CSR_OK) {
        fprintf(stderr, // obs-ok:pre-existing-diagnostic
                "[boot] csr rejected header promotion (%s) "
                "reason=%s h=%d\n",
                csr_result_name(rc), reason ? reason : "", header->nHeight);
        return false;
    }
    return true;
}

/* DERIVED coins-best (wave 2): one cheap point-read of progress.kv's own
 * co-committed state via reducer_frontier_derive_coins_best. Recomputed at
 * every decision point (derive, don't cache). Returns true iff
 * coins_applied_height is present — the canonical-datadir signal that the
 * legacy node_state/mirror anchors are mere caches and every legacy
 * anchor-repair rung below must be skipped. */
struct boot_derived_coins_best {
    int32_t height;      /* coins_applied_height - 1 */
    uint8_t hash[32];    /* valid iff hash_found */
    bool hash_found;
};

static bool boot_derive_coins_best(struct boot_derived_coins_best *out)
{
    memset(out, 0, sizeof(*out));
    out->height = -1;
    return reducer_frontier_derive_coins_best_now(&out->height, out->hash,
                                                  &out->hash_found);
}

static bool boot_step_init_crypto_and_state(struct app_context *ctx,
                                             const struct chain_params *params)
{
    /* Assumevalid is set after block index loads (see below — first the
     * default-from-checkpoint case fires here, the user-provided-hash
     * case is deferred). The implementation in contextual_check_tx.c
     * handles both script verification (connect_block.c) and Sapling
     * proof verification (contextual_check_tx.c) via
     * g_deferred_proof_validation_below_height. */

    ecc_start();
    ecc_verify_init();

    /* SHA-256 hardware self-test */
    if (!sha256_selftest())
        printf("WARNING: SHA-256 SHA-NI self-test FAILED — using portable fallback\n");
    printf("SHA-256: %s\n", sha256_implementation());

    /* Report field arithmetic acceleration */
    extern const char *fr_accel_implementation(void);
    printf("Field arithmetic: %s\n", fr_accel_implementation());

    main_state_init(&g_state);
    g_state.fTxIndex = ctx->tx_index;
    g_state.fCheckpointsEnabled = ctx->checkpoints_enabled;

    /* Initialize chain activation controller — single authority for when the
     * reducer activation path can run. Must be before any chain work. */
    activation_controller_init(&g_activation_ctl, &g_state, &g_coins_tip,
                               params, ctx->datadir);
    activation_set_state(&g_activation_ctl, ACTIVATION_BOOT_PENDING,
                         "boot_start");

    /* -deferproofvalidationbelow: skip Groth16/Sapling proof verification for blocks
     * at or below the specified hash's height. Default: latest checkpoint.
     * Pass -deferproofvalidationbelow=0 to disable (verify everything). */
    if (ctx->defer_proof_validation_below && strcmp(ctx->defer_proof_validation_below, "0") == 0) {
        g_deferred_proof_validation_below_height = -1;
        printf("Deferred proof validation: disabled (verifying all proofs)\n");
    } else if (ctx->defer_proof_validation_below) {
        /* Resolve user-provided hash after block index loads (deferred) */
    } else {
        /* Default: latest checkpoint height */
        if (params->checkpointData.nEntries > 0) {
            g_deferred_proof_validation_below_height =
                params->checkpointData.entries[params->checkpointData.nEntries - 1].height;
            printf("Deferred proof validation: height %d (latest checkpoint)\n",
                   g_deferred_proof_validation_below_height);
        }
    }

    /* Defer ZK key loading to background thread — not needed for RPC startup.
     * Keys load in parallel while block index + wallet initialize. */
    g_params_thread_started = false;
    if (ctx->params_dir) {
        snprintf(g_params_dir_buf, sizeof(g_params_dir_buf), "%s", ctx->params_dir);
        /* One-shot ZK params loader; joined at shutdown via
         * app_shutdown's params_thread field. raw-pthread-ok */
        if (pthread_create(&g_params_thread, NULL, load_params_thread, NULL) == 0)
            g_params_thread_started = true;
        else
            fprintf(stderr,
                    "WARNING: failed to start ZK params loader thread\n");
    }

    boot_stage_advance_to(BOOT_STAGE_CRYPTO_READY);
    return true;
}

static bool boot_disk_monitor_service_start(void *ctx)
{
    const char *datadir = ctx;
    if (!datadir)
        return false;
    disk_monitor_config_defaults(&g_disk_monitor_cfg);
    g_disk_monitor_cfg.datadir = datadir;
    struct zcl_result dr = disk_monitor_start(&g_disk_monitor_cfg);
    if (dr.ok) {
        printf("Disk monitor started (warn=%lldGB refuse=%lldGB)\n",
               (long long)(g_disk_monitor_cfg.warn_free_bytes >> 30),
               (long long)(g_disk_monitor_cfg.refuse_free_bytes >> 30));
        return true;
    }
    fprintf(stderr, "[boot] %s:%d disk_monitor_start failed: code=%d %s\n",
            dr.source_file, dr.source_line, dr.code, dr.message);
    return false;
}

static void boot_disk_monitor_service_stop(void *ctx)
{
    (void)ctx;
    disk_monitor_stop();
}

static bool boot_ibd_throttle_service_start(void *ctx)
{
    (void)ctx;
    ibd_throttle_config_defaults(&g_ibd_throttle_cfg);
    ibd_throttle_config_from_env(&g_ibd_throttle_cfg);
    struct zcl_result ir = ibd_throttle_start(&g_ibd_throttle_cfg);
    if (ir.ok) {
        printf("IBD throttle started (rate=%lld/s burst=%lld)\n",
               (long long)g_ibd_throttle_cfg.blocks_per_sec,
               (long long)g_ibd_throttle_cfg.burst);
        return true;
    }
    fprintf(stderr, "[boot] %s:%d ibd_throttle_start failed: code=%d %s\n",
            ir.source_file, ir.source_line, ir.code, ir.message);
    return false;
}

static void boot_ibd_throttle_service_stop(void *ctx)
{
    (void)ctx;
    ibd_throttle_stop();
}

static bool boot_wallet_backup_service_start(void *ctx)
{
    struct node_db *db = ctx;
    static char backup_dir[1024];

    if (!db || !db->open || !db->db) {
        printf("Wallet backup deferred until node DB is open\n");
        return true;
    }

    /* Defaults apply the WALLET_BACKUP_PASSWORD encryption policy. */
    wallet_backup_config_defaults(&g_wallet_backup_cfg);

    if (!g_wallet_backup_cfg.backup_dir) {
        const char *home = getenv("HOME");
        snprintf(backup_dir, sizeof(backup_dir), "%s/wallet_backups",
                 home && home[0] ? home : ".");
        g_wallet_backup_cfg.backup_dir = backup_dir;
    }
    struct zcl_result br = wallet_backup_start(&g_wallet_backup_cfg, db);
    if (br.ok) {
        printf("Wallet backup started (interval=%ds max=%d encrypt=%s)\n",
               g_wallet_backup_cfg.interval_seconds, g_wallet_backup_cfg.max_versions,
               g_wallet_backup_cfg.encrypt ? "on" : "off");
        return true;
    }
    fprintf(stderr, "[boot] %s:%d wallet_backup_start failed: code=%d %s\n",
            br.source_file, br.source_line, br.code, br.message);
    return false;
}

static void boot_wallet_backup_service_stop(void *ctx)
{
    (void)ctx;
    wallet_backup_stop();
}

static bool boot_db_maintenance_service_start(void *ctx)
{
    struct node_db *db = ctx;
    if (!db || !db->open)
        return true;

    struct db_maintenance_schedule dbm_sched;
    db_maintenance_schedule_defaults(&dbm_sched);
    dbm_sched.wal_checkpoint_minutes = 5;
    struct zcl_result _dbm_r = db_maintenance_start(db, &dbm_sched);
    if (_dbm_r.ok) {
        printf("DB maintenance started (wal=%dmin analyze=%dh)\n",
               dbm_sched.wal_checkpoint_minutes,
               dbm_sched.analyze_hours);
        return true;
    }
    fprintf(stderr, "db_maintenance_start failed: %s\n", _dbm_r.message);
    return false;
}

static void boot_db_maintenance_service_stop(void *ctx)
{
    (void)ctx;
    db_maintenance_stop();
}

static bool boot_register_guard_services(const char *datadir)
{
    const struct zcl_service_spec disk_spec = {
        .name = "disk_monitor",
        .start = boot_disk_monitor_service_start,
        .stop = boot_disk_monitor_service_stop,
        .ctx = (void *)datadir,
        .flags = ZCL_SERVICE_OPTIONAL,
    };
    const struct zcl_service_spec ibd_spec = {
        .name = "ibd_throttle",
        .start = boot_ibd_throttle_service_start,
        .stop = boot_ibd_throttle_service_stop,
        .flags = ZCL_SERVICE_OPTIONAL,
    };
    return zcl_service_kernel_register(&g_guard_kernel, &disk_spec) &&
           zcl_service_kernel_register(&g_guard_kernel, &ibd_spec);
}

static bool boot_register_maintenance_services(void)
{
    const struct zcl_service_spec wallet_backup_spec = {
        .name = "wallet_backup",
        .start = boot_wallet_backup_service_start,
        .stop = boot_wallet_backup_service_stop,
        .ctx = &g_node_db,
        .flags = ZCL_SERVICE_OPTIONAL,
    };
    const struct zcl_service_spec db_maintenance_spec = {
        .name = "db_maintenance",
        .start = boot_db_maintenance_service_start,
        .stop = boot_db_maintenance_service_stop,
        .ctx = &g_node_db,
        .flags = ZCL_SERVICE_OPTIONAL,
    };
    return zcl_service_kernel_register(&g_maintenance_kernel,
                                       &wallet_backup_spec) &&
           zcl_service_kernel_register(&g_maintenance_kernel,
                                       &db_maintenance_spec);
}

static void boot_step_start_disk_and_ibd_guards(const char *datadir)
{
    zcl_service_kernel_init(&g_guard_kernel);
    if (!boot_register_guard_services(datadir) ||
        !zcl_service_kernel_start_all(&g_guard_kernel)) {
        fprintf(stderr, "WARNING: failed to start boot guard services\n");
    }
}

static void boot_step_start_maintenance_services(void)
{
    zcl_service_kernel_init(&g_maintenance_kernel);
    if (!boot_register_maintenance_services() ||
        !zcl_service_kernel_start_all(&g_maintenance_kernel)) {
        fprintf(stderr, "WARNING: failed to start maintenance services\n");
    }
}

static void boot_stop_platform_services(void)
{
    zcl_service_kernel_stop_all(&g_maintenance_kernel);
    zcl_service_kernel_stop_all(&g_guard_kernel);
    zcl_service_kernel_reset(&g_maintenance_kernel);
    zcl_service_kernel_reset(&g_guard_kernel);
}

static bool boot_db_worker_service_init(struct zcl_service_kernel *kernel,
                                        void *ctx)
{
    (void)kernel;
    (void)ctx;
    return db_service_attach(&g_db_service, &g_node_db);
}

static bool boot_db_worker_service_start(void *ctx)
{
    (void)ctx;
    if (db_service_start(&g_db_service)) {
        memset(&g_boot_runtime, 0, sizeof(g_boot_runtime));
        g_boot_runtime.db_service = &g_db_service;
        app_runtime_set_current(&g_boot_runtime);
        return true;
    }
    return false;
}

static void boot_db_worker_service_stop(void *ctx)
{
    (void)ctx;
    db_service_stop(&g_db_service);
}

static bool boot_step_start_db_service(void)
{
    const struct zcl_service_spec db_spec = {
        .name = "db_service",
        .init = boot_db_worker_service_init,
        .start = boot_db_worker_service_start,
        .stop = boot_db_worker_service_stop,
    };

    zcl_service_kernel_init(&g_boot_db_kernel);
    return zcl_service_kernel_register(&g_boot_db_kernel, &db_spec) &&
           zcl_service_kernel_start_all(&g_boot_db_kernel);
}

void boot_stop_db_service_kernel(void)
{
    zcl_service_kernel_stop_all(&g_boot_db_kernel);
    zcl_service_kernel_reset(&g_boot_db_kernel);
}

bool app_init(struct app_context *ctx)
{
    int64_t t_boot_start = boot_clock_ms();
    int64_t t_phase;

    /* ── Move 5 boot checklist: prologue steps ───────────────────
     * Each step is a named static above. Failing steps return false
     * and the process exits non-zero — never partial-init state. */
    if (!boot_step_init_observability())
        return false;
    if (!boot_step_select_chain_and_datadir(ctx))
        return false;
    const struct chain_params *params = chain_params_get();

    boot_step_init_postmortem(ctx->datadir);
    boot_step_detect_unclean_shutdown(ctx->datadir);
    boot_step_start_disk_and_ibd_guards(ctx->datadir);

    if (!boot_step_init_crypto_and_state(ctx, params))
        return false;

    /* Timing only (no behavior change): the prologue above
     * (observability, chain/datadir select, postmortem, unclean-shutdown
     * detect, disk/IBD guards, crypto+state init) was previously
     * uninstrumented — part of the ~20s warm-start "unattributed gap" in
     * docs/work/sync-perf-profile-2026-05-29.md. Emit it as a named phase
     * matching the existing [boot] <phase> Nms idiom. */
    printf("[boot] %-30s %lldms\n", "prologue",
           (long long)(boot_clock_ms() - t_boot_start));

    /* Initialize wallet (before block index — needed for -importlegacy).
     *
     * Wallet persistence boot state machine (per WALLET_PERSISTENCE_PLAN.md §7).
     *
     *   STATE A — node.db absent:          generate keypool, flush.
     *   STATE B — wallet_keys missing:     CREATE (done at DB open), flush.
     *   STATE C — wallet_keys non-empty, open OK:  load, canary, verify count.
     *   STATE D — wallet_keys non-empty, open FAILS: ABORT.
     *   STATE E — canary self-test fails on existing wallet:       ABORT.
     *   STATE F — loaded keystore count != on-disk row count:      ABORT.
     *
     * D/E/F are the paths the pre-fix code took silently, overwriting
     * the user's wallet with a fresh keypool. Refuse to do that here. */
    t_phase = boot_clock_ms();
    wallet_init(&g_wallet);

    /* Determine the on-disk wallet_keys row count BEFORE attempting to
     * open the wallet_sqlite subsystem. Used below for the abort
     * decisions — we only refuse to proceed when there's something to
     * lose. */
    int64_t pre_open_key_rows = 0;          /* 0 = empty; -1 = cannot determine */
    if (g_node_db.open) {
        sqlite3_stmt *wk_count = NULL;
        if (sqlite3_prepare_v2(g_node_db.db,
                "SELECT count(*) FROM wallet_keys",
                -1, &wk_count, NULL) == SQLITE_OK && wk_count) {
            if (sqlite3_step(wk_count) == SQLITE_ROW)
                pre_open_key_rows = sqlite3_column_int64(wk_count, 0);
            sqlite3_finalize(wk_count);
        } else {
            /* wallet_keys table absent — fell through to STATE B. */
            pre_open_key_rows = -1;
        }
    }

    /* Attempt to open — use the rich-error *_r API (Agent 2) so any
     * prepare failure carries a WSQL_* code + message + source
     * location we can surface in the abort diagnostic. */
    struct zcl_result wsql_open_r = {0};
    bool sqlite_open = false;
    if (g_node_db.open) {
        wsql_open_r = wallet_sqlite_open_r(&g_wallet_sqlite, g_node_db.db);
        sqlite_open = wsql_open_r.ok;
    }

    if (!sqlite_open && pre_open_key_rows > 0) {
        /* STATE D: wallet has user keys on disk but we cannot open the
         * persistence layer. The ONLY safe action is to refuse the
         * boot and preserve the datadir for the operator to
         * investigate. Silently generating a fresh keypool here is
         * what caused 0.4 ZCL to become unspendable. */
        fprintf(stderr,
            "\nFATAL: wallet persistence initialisation failed.\n"
            "       code=%d\n"
            "       message=%s\n"
            "       source=%s:%d\n"
            "       node.db contains %lld existing wallet_keys rows —"
            " REFUSING to regenerate.\n"
            "       To recover: see WALLET_PERSISTENCE_RECOVERY.md\n\n",
            wsql_open_r.code,
            wsql_open_r.message[0] ? wsql_open_r.message
                                   : "wallet_sqlite_open_r returned !ok",
            wsql_open_r.source_file ? wsql_open_r.source_file
                                    : "config/src/boot.c",
            wsql_open_r.source_line,
            (long long)pre_open_key_rows);
        event_emitf(EV_BOOT_VALIDATION_FAILED, 0,
                    "wallet_persistence_open_failed code=%d rows=%lld",
                    wsql_open_r.code, (long long)pre_open_key_rows);
        exit(1);
    }

    /* STATE C: load everything. */
    if (sqlite_open) {
        {
            struct zcl_result _r = wallet_sqlite_read_keys_r(
                &g_wallet_sqlite, &g_wallet);
            if (!_r.ok) {
                fprintf(stderr,
                    "wallet_sqlite_read_keys failed: code=%d %s (%s:%d)\n",
                    _r.code, _r.message,
                    _r.source_file ? _r.source_file : "?", _r.source_line);
            }
        }
        wallet_sqlite_read_txs(&g_wallet_sqlite, &g_wallet);
        wallet_rebuild_spent_set(&g_wallet);
        wallet_sqlite_read_sapling_keys(&g_wallet_sqlite, &g_wallet);
        wallet_sqlite_read_scripts(&g_wallet_sqlite, &g_wallet);
        wallet_sqlite_read_watch_only(&g_wallet_sqlite, &g_wallet);
        int saved_height = 0;
        if (wallet_sqlite_read_scan_height(&g_wallet_sqlite, &saved_height))
            g_wallet.best_block_height = saved_height;
        printf("Wallet loaded: %zu keys, %zu sapling keys, %zu scripts, "
               "%zu watch-only, %zu txs, scan height %d.\n",
               g_wallet.keystore.num_keys,
               g_wallet.sapling_keys.num_keys,
               g_wallet.keystore.num_scripts,
               g_wallet.keystore.num_watching,
               g_wallet.num_wallet_tx,
               g_wallet.best_block_height);

        /* STATE E: canary self-test. Writes then reads a fresh random
         * probe through the same sqlite handle the node will use for
         * user RPCs. A failure here is STATE E — if keys exist on
         * disk, abort rather than risk a silent overwrite. */
        struct wallet_canary_status cs;
        int crc = wallet_canary_run(g_node_db.db, &cs);
        if (crc != WALLET_CANARY_OK) {
            if (pre_open_key_rows > 0) {
                fprintf(stderr,
                    "\nFATAL: wallet canary self-test failed.\n"
                    "       code=%d\n"
                    "       message=%s\n"
                    "       source=lib/wallet/src/wallet_canary.c\n"
                    "       node.db contains %lld existing wallet_keys rows —"
                    " REFUSING to proceed.\n"
                    "       To recover: see WALLET_PERSISTENCE_RECOVERY.md\n\n",
                    crc, cs.error, (long long)pre_open_key_rows);
                event_emitf(EV_BOOT_VALIDATION_FAILED, 0,
                            "wallet_canary_failed code=%d rows=%lld",
                            crc, (long long)pre_open_key_rows);
                exit(1);
            }
            fprintf(stderr,
                "WARNING: wallet canary failed (code=%d): %s —"
                " continuing on empty wallet.\n", crc, cs.error);
        }

        /* STATE F: invariant — the keystore count loaded from disk
         * must equal the row count we observed before opening. A
         * mismatch means read_keys dropped rows or the table changed
         * under us. Either is a bug that would become silent data
         * loss on the next flush. */
        if (pre_open_key_rows > 0 &&
            (int64_t)g_wallet.keystore.num_keys != pre_open_key_rows) {
            fprintf(stderr,
                "\nFATAL: wallet keystore count mismatch.\n"
                "       wallet_keys rows=%lld\n"
                "       loaded keystore=%zu\n"
                "       source=config/src/boot.c\n"
                "       REFUSING to proceed — in-memory and on-disk diverged.\n"
                "       To recover: see WALLET_PERSISTENCE_RECOVERY.md\n\n",
                (long long)pre_open_key_rows, g_wallet.keystore.num_keys);
            event_emitf(EV_BOOT_VALIDATION_FAILED, 0,
                        "wallet_keystore_count_mismatch rows=%lld loaded=%zu",
                        (long long)pre_open_key_rows,
                        g_wallet.keystore.num_keys);
            exit(1);
        }
    } else {
        /* STATE A/B: new datadir, no user keys at risk. */
        printf("New wallet created.\n");
    }

    /* One-time wallet migration: if SQLite wallet is empty but LevelDB
     * wallet/ directory exists, import keys/txs from LevelDB. Only
     * runs when pre_open_key_rows <= 0 (no existing user keys), so no
     * conflict with the state machine. */
    if (g_wallet.keystore.num_keys == 0) {
        char wallet_path[1024];
        snprintf(wallet_path, sizeof(wallet_path), "%s/wallet", ctx->datadir);
        struct stat wst;
        if (stat(wallet_path, &wst) == 0) {
            struct wallet_db legacy_wdb;
            if (wallet_db_open(&legacy_wdb, wallet_path)) {
                printf("Migrating wallet from LevelDB...\n");
                wallet_db_read_keys(&legacy_wdb, &g_wallet);
                wallet_db_read_txs(&legacy_wdb, &g_wallet);
                wallet_rebuild_spent_set(&g_wallet);
                wallet_db_read_sapling_keys(&legacy_wdb, &g_wallet);
                wallet_db_read_scripts(&legacy_wdb, &g_wallet);
                int saved_height = 0;
                if (wallet_db_read_scan_height(&legacy_wdb, &saved_height))
                    g_wallet.best_block_height = saved_height;
                wallet_db_close(&legacy_wdb);

                /* Persist to SQLite. Unlike the old code, we check
                 * the result — a silent failure here is the exact
                 * bug the state machine defends against. */
                if (g_wallet_sqlite.open) {
                    struct zcl_result _r = wallet_sqlite_flush_r(
                        &g_wallet_sqlite, &g_wallet);
                    if (!_r.ok) {
                        fprintf(stderr,
                            "\nFATAL: LevelDB wallet migration flush failed.\n"
                            "       code=%d message=%s\n"
                            "       source=%s:%d\n"
                            "       REFUSING to proceed — keys would be RAM-only.\n\n",
                            _r.code, _r.message,
                            _r.source_file ? _r.source_file : "?",
                            _r.source_line);
                        event_emitf(EV_BOOT_VALIDATION_FAILED, 0,
                                    "wallet_migration_flush_failed code=%d",
                                    _r.code);
                        exit(1);
                    }
                }

                printf("Wallet migrated: %zu keys, %zu sapling keys\n",
                       g_wallet.keystore.num_keys,
                       g_wallet.sapling_keys.num_keys);
            }
        }
    }

    if (g_wallet.keystore.num_keys == 0) {
        /* Genuinely empty wallet — generate the initial keypool and
         * flush. This is STATE A/B's terminal action. */
        wallet_top_up_key_pool(&g_wallet, DEFAULT_KEYPOOL_SIZE);
        if (g_wallet_sqlite.open) {
            struct zcl_result _r = wallet_sqlite_flush_r(
                &g_wallet_sqlite, &g_wallet);
            if (!_r.ok) {
                fprintf(stderr,
                    "\nFATAL: initial keypool flush failed.\n"
                    "       code=%d message=%s\n"
                    "       source=%s:%d\n"
                    "       REFUSING to proceed — fresh keys would be RAM-only.\n\n",
                    _r.code, _r.message,
                    _r.source_file ? _r.source_file : "?",
                    _r.source_line);
                event_emitf(EV_BOOT_VALIDATION_FAILED, 0,
                            "wallet_keypool_flush_failed code=%d", _r.code);
                exit(1);
            }
        }
        if (g_node_db.open)
            node_db_wal_checkpoint(&g_node_db);
        printf("New wallet created.\n");
    }
    printf("Wallet has %zu keys.\n", g_wallet.keystore.num_keys);
    printf("[boot] %-30s %lldms\n", "wallet_load",
           (long long)(boot_clock_ms() - t_phase));

    boot_step_start_maintenance_services();

    /* Pre-flight: check for stale lock files from crashed processes */
    {
        char lock_path[1024];

        /* Check LevelDB locks */
        snprintf(lock_path, sizeof(lock_path), "%s/blocks/index/LOCK",
                 ctx->datadir);
        if (access(lock_path, F_OK) == 0) {
            /* LevelDB LOCK exists — check if holder is still alive */
            FILE *lf = fopen(lock_path, "r");
            if (lf) {
                char pidbuf[32] = {0};
                size_t nr = fread(pidbuf, 1, sizeof(pidbuf) - 1, lf);
                fclose(lf);
                if (nr > 0) {
                    long pid = strtol(pidbuf, NULL, 10);
                    if (pid > 0 && kill((pid_t)pid, 0) != 0) {
                        printf("Removing stale LevelDB LOCK (pid %ld dead)\n",
                               pid);
                        unlink(lock_path);
                    } else if (pid > 0) {
                        fprintf(stderr,
                            "ERROR: LevelDB locked by pid %ld (still running)\n"
                            "Kill the other process or use a different datadir.\n",
                            pid);
                    }
                }
            }
        }
        snprintf(lock_path, sizeof(lock_path), "%s/chainstate/LOCK",
                 ctx->datadir);
        if (access(lock_path, F_OK) == 0) {
            FILE *lf = fopen(lock_path, "r");
            if (lf) {
                char pidbuf[32] = {0};
                size_t nr = fread(pidbuf, 1, sizeof(pidbuf) - 1, lf);
                fclose(lf);
                if (nr > 0) {
                    long pid = strtol(pidbuf, NULL, 10);
                    if (pid > 0 && kill((pid_t)pid, 0) != 0) {
                        printf("Removing stale chainstate LOCK (pid %ld dead)\n",
                               pid);
                        unlink(lock_path);
                    }
                }
            }
        }

        /* Check SQLite WAL lock — WAL mode handles this via busy_timeout,
         * but a crash can leave a -wal file. SQLite recovers automatically. */
        snprintf(lock_path, sizeof(lock_path), "%s/node.db-wal",
                 ctx->datadir);
        if (access(lock_path, F_OK) == 0)
            printf("SQLite WAL file exists (normal after crash recovery)\n");
    }

    /* Open SQLite node database */
    t_phase = boot_clock_ms();
    if (node_db_sync_init(&g_node_db, ctx->datadir)) {
        int migrate_rc = node_db_migrate(&g_node_db, ctx->datadir);
        if (migrate_rc == -2) {
            /* Schema downgrade refused — node_db_migrate has already
             * printed the operator-actionable explanation to stderr.
             * Bail before any wallet / chain logic runs so we don't
             * write through a schema we don't understand. */
            event_emitf(EV_BOOT_VALIDATION_FAILED, 0,
                        "node_db_schema_downgrade_refused rc=%d", migrate_rc);
            exit(1);
        }
        process_block_set_node_db(&g_node_db);
        if (!db_service_is_started(&g_db_service)) {
            if (!boot_step_start_db_service()) {
                fprintf(stderr,
                    "Warning: DB service unavailable during boot; "
                    "activation metadata writes will use direct SQLite\n");
            }
        }
        boot_stage_advance_to(BOOT_STAGE_DB_OPEN);
        int db_tip = node_db_sync_get_tip_height(&g_node_db);
        if (db_tip >= 0) {
            printf("SQLite tip: height=%d\n", db_tip);
            event_emitf(EV_BOOT_DB_OPEN, 0, "schema=%d tip=%d",
                        node_db_schema_version(&g_node_db), db_tip);
        }
    } else {
        fprintf(stderr, "Warning: SQLite database unavailable\n");
        event_emitf(EV_DB_ERROR, 0, "SQLite open failed at %s/node.db",
                    ctx->datadir);
    }
    printf("[boot] %-30s %lldms\n", "sqlite_open_migrate",
           (long long)(boot_clock_ms() - t_phase));

    /* Open the dedicated progress.kv SQLite file that hosts every
     * staged-sync stage cursor. Independent of node.db so
     * cursor commits stay off the hot path of larger transactions. */
    if (!progress_store_open(ctx->datadir)) {
        fprintf(stderr,
            "Warning: progress_store unavailable; staged-sync stages will "
            "not be able to persist cursors\n");
    } else {
        /* Restore the prior operational mode for observability. Non-fatal;
         * happens before any reducer stage so there is no race with stage
         * cursor use. Boot overwrites this with the real post-restore mode
         * (DEGRADED_SERVING / SYNCING) further down. */
        (void)service_state_restore_from_progress_store();
    }

    /* Snapshot-first: if a downloaded consensus_snapshot.db
     * is present in the datadir, import its UTXOs into node.db *before*
     * any chain-tip restoration runs. This makes coins_best_block
     * resolve to the snapshot height when the coins view first reads
     * it, so utxo_recovery_restore_chain_tip / chain_restore_finalize
     * observe the snapshot anchor as ground truth instead of racing
     * past it with the older block_index.bin tip and leaving utxos=0.
     *
     * Idempotent: the helper refuses to import if main.utxos already
     * holds the snapshot's contents (handled by checking the source
     * file's integrity + size; a re-run with utxos>1000 is a no-op via
     * the export guard in consensus_snapshot_export_service_run). */
    if (g_node_db.open) {
        char snap_path[PATH_MAX];
        int sp_n = snprintf(snap_path, sizeof(snap_path),
                            "%s/consensus_snapshot.db", ctx->datadir);
        if (sp_n > 0 && (size_t)sp_n < sizeof(snap_path)) {
            struct stat sp_st;
            if (stat(snap_path, &sp_st) == 0 &&
                sp_st.st_size > (off_t)(10 * 1024 * 1024)) {
                /* Wave 2: the canonical coin count lives in coins_kv;
                 * the node.db mirror count is the legacy fallback. */
                int64_t existing = coins_kv_count(progress_store_db());
                if (existing <= 0)
                    existing = node_db_utxo_count(&g_node_db);
                if (existing > 1000) {
                    printf("[boot] consensus_snapshot.db present "
                           "(%.0f MB) — node.db already has %lld UTXOs, "
                           "skipping pre-restore import\n",
                           (double)sp_st.st_size / (1024.0 * 1024.0),
                           (long long)existing);
                    chain_restore_record_snapshot_import(
                        true, existing, -1);
                } else {
                    int64_t imp_utxos = 0, imp_height = 0;
                    uint8_t imp_best[32] = {0};
                    bool ok = boot_import_snapshot_db(&g_node_db,
                                                     snap_path,
                                                     &imp_utxos,
                                                     &imp_height,
                                                     imp_best);
                    chain_restore_record_snapshot_import(
                        ok, imp_utxos, imp_height);
                    event_emitf(EV_BOOT_UTXO_IMPORT, 0,
                                "phase=pre-restore ok=%d utxos=%lld "
                                "height=%lld",
                                ok ? 1 : 0,
                                (long long)imp_utxos,
                                (long long)imp_height);
                    if (ok)
                        printf("[boot] snapshot-first import OK: "
                               "%lld UTXOs at h=%lld — chain restore "
                               "will observe snapshot anchor\n",
                               (long long)imp_utxos,
                               (long long)imp_height);
                    else
                        fprintf(stderr,  // obs-ok:helper-context-logged
                                "[boot] snapshot-first import failed "
                                "for %s — falling through to block-by-block "
                                "IBD path\n", snap_path);
                }
            }
        }
    }

    /* -snapshot: Create snapshot of legacy data dir, import in parallel,
     * then start normally with P2P sync to catch up any delta. */
    if (ctx->snapshot_dir) {
        if (!g_node_db.open) {
            fprintf(stderr, "Error: SQLite database required for snapshot\n");
            return false;
        }

        /* Step 1: Create snapshot (hardlink block files, copy LevelDB) */
        const char *snap = snapshot_create(ctx->snapshot_dir,
                                           ctx->datadir, 2);
        if (!snap) {
            fprintf(stderr, "Error: Failed to create snapshot\n");
            return false;
        }

        /* Step 2: Parallel import (block index + UTXOs + wallet) */
        if (snapshot_import(snap, ctx->datadir,
                            &g_node_db, &g_wallet) < 0) {
            fprintf(stderr, "Warning: Snapshot import had errors\n");
        }

        /* Step 3: Build transaction index after runtime services take
         * ownership of background jobs, so shutdown can join it cleanly. */
    }


    /* Open block index database.
     * Remove stale LOCK files — left behind by unclean legacy import exit. */
    char blocktree_path[1024];
    snprintf(blocktree_path, sizeof(blocktree_path), "%s/blocks/index",
             ctx->datadir);
    {
        char lock_path[1100];
        snprintf(lock_path, sizeof(lock_path), "%s/LOCK", blocktree_path);
        unlink(lock_path); /* harmless if doesn't exist */
    }
    /* Remove scratch dirs a crashed prior boot may have stranded: a kill -9
     * mid legacy-chainstate copy leaves chainstate_import_tmp /
     * .legacy_ldb_snap on disk forever (their only cleanup is on the
     * normal-return path of the import that created them). They are
     * recreated from scratch whenever the import re-runs, so a torn copy
     * has no recovery value — same proactive hygiene as the stale LOCK
     * unlink above. */
    {
        const char *scratch[] = { "chainstate_import_tmp",
                                  ".legacy_ldb_snap" };
        for (size_t si = 0; si < sizeof(scratch) / sizeof(scratch[0]); si++) {
            char spath[1100];
            snprintf(spath, sizeof(spath), "%s/%s", ctx->datadir,
                     scratch[si]);
            struct stat sst;
            if (stat(spath, &sst) == 0 && S_ISDIR(sst.st_mode)) {
                char cmd[1300];
                snprintf(cmd, sizeof(cmd), "rm -rf '%s'", spath);
                int rc = system(cmd); // obs-ok:boot-scratch-hygiene
                printf("[boot] removed stranded scratch dir %s (rc=%d)\n",
                       spath, rc);
            }
        }
    }
    if (block_tree_db_open(&g_block_tree, blocktree_path,
                           256 << 20, false, false)) {
        g_block_tree_open = true;
        g_active_block_tree = &g_block_tree;
    } else {
        fprintf(stderr, "Warning: Could not open block tree DB at %s\n",
                blocktree_path);
    }

    /* Open coins view on the SHARED sqlite3 handle.
     * Both node_db and coins_view_sqlite use the same connection.
     * Transaction coordination is handled by flush_coins_if_needed
     * which commits node_db's batch before the coins flush runs
     * its own BEGIN/COMMIT. One connection = no WAL lock contention. */
    if (g_node_db.open) {
        /* -reindex-chainstate explicitly rebuilds the UTXO set from on-disk
         * block data, discarding the stored coins state. Clear that state
         * BEFORE the coins-integrity gate runs — otherwise a torn coins anchor
         * (the §3 wedge) FATAL-halts boot before reindex_chainstate (which
         * performs the same wipe idempotently, ~line 2539) can run the rebuild
         * the operator asked for. Guarded strictly on the explicit request: a
         * normal boot never wipes a recoverable coins set here. */
        if (ctx->reindex_chainstate && boot_index_clear_coins_state(&g_node_db))
            fprintf(stderr,
                "[boot] -reindex-chainstate: cleared coins state before the "
                "integrity gate; UTXO set will be rebuilt from block data\n");
        /* Wave 2: on canonical datadirs (coins_applied_height present) the
         * coins-best fact is DERIVED from coins_kv's own co-committed state —
         * the legacy anchor caches cannot wedge boot, so the legacy repair
         * rungs (stale-cursor repair W4, torn-anchor heal W5 — both
         * node_state-key writers) are skipped entirely. Legacy datadirs keep
         * both rungs unchanged. */
        struct boot_derived_coins_best boot_dcb;
        bool boot_dcb_found = boot_derive_coins_best(&boot_dcb);
        if (boot_dcb_found)
            printf("[boot] derived coins-best h=%d hash_found=%d (coins_kv "
                   "authority) — skipping legacy anchor-repair rungs\n",
                   boot_dcb.height, boot_dcb.hash_found ? 1 : 0);
        else
            (void)utxo_recovery_repair_stale_cursor_from_sync_projection(
                &g_node_db);
        /* On open failure, try L1 torn-legacy-coins recovery (§3 dual-store
         * tear, utxo_recovery_torn_anchor.c — reset-safe, refuses unless
         * coins_kv is the proven authority, so the FATAL never weakens), then
         * retry. If neither path recovers, refuse to start with a crash event. */
        if (!coins_view_sqlite_open(&g_coins_sqlite, g_node_db.db) &&
            !(!boot_dcb_found &&
              utxo_recovery_heal_torn_legacy_coins_anchor(
                  &g_node_db, progress_store_db(), ctx->datadir) &&
              coins_view_sqlite_open(&g_coins_sqlite, g_node_db.db))) {
            fprintf(stderr,
                "FATAL: coins view integrity check failed — the "
                "UTXO set is inconsistent with the stored tip "
                "anchor and the auto-rewind guard did not recover "
                "it.  The node will not start.  Operator action: "
                "restore ~/.zclassic-c23/node.db from a known-good "
                "backup, OR delete it and allow a full resync.\n");
            event_emitf(EV_BOOT_VALIDATION_FAILED, 0,
                        "coins_view tip mismatch exceeds auto-rewind guard");
            _exit(EXIT_FAILURE);
        }
    }

    /* One-time migration: import UTXOs from LevelDB chainstate into SQLite.
     * The old LevelDB had the authoritative UTXO set; SQLite's utxos table
     * may be incomplete. Check for migration flag in node_state. */
    if (g_node_db.open && g_coins_sqlite.db) {
        /* Do not seed coins_best_block from the sync projection cursor.
         * Only do this if UTXOs exist AND no LDB chainstate is available.
         * If chainstate/ exists, the LDB import will set coins_best_block
         * correctly — seeding from a projection cursor would create a mismatch
         * (UTXO data from LDB height ~3M labeled as chain tip ~2M). */
        struct boot_derived_coins_best seed_dcb;
        if (boot_derive_coins_best(&seed_dcb)) {
            /* Canonical datadir: the unset/stale cache key is irrelevant —
             * the coins-best fact is derived. Nothing to seed or refuse. */
            printf("[boot] coins_best_block cache check skipped — derived "
                   "coins-best h=%d (coins_kv authority)\n", seed_dcb.height);
        } else {
            struct uint256 coins_check;
            memset(&coins_check, 0, sizeof(coins_check));
            if (!coins_view_sqlite_get_best_block(&g_coins_sqlite,
                                                  &coins_check)
                || uint256_is_null(&coins_check)) {
                /* Check if LDB chainstate exists — if so, skip the seed
                 * and let LDB import set coins_best_block properly. */
                char cs_path[576];
                snprintf(cs_path, sizeof(cs_path), "%s/chainstate",
                         ctx->datadir);
                struct stat cs_st;
                bool has_chainstate = (stat(cs_path, &cs_st) == 0 &&
                                        S_ISDIR(cs_st.st_mode));
                if (has_chainstate) {
                    printf("[boot] chainstate/ exists — skipping "
                           "coins_best_block seed (LDB import will set it)\n");
                } else {
                    int64_t utxo_count = node_db_utxo_count(&g_node_db);
                    if (utxo_count > 0) {
                        printf("[boot] coins_best_block is unset with %lld "
                               "UTXOs; refusing to seed it from sync "
                               "projection\n", (long long)utxo_count);
                    }
                }
            }
        }

        /* Auto-recovery: check for needs_reimport flag */
        if (utxo_reimport_flag_check_and_clear(ctx->datadir))
            ctx->reimport_utxos = true;

        /* Crash-only: consume a prior boot's self-rebuild request (reindex). */
        if (boot_crashonly_consume_reindex_request(ctx->datadir))
            ctx->reindex_chainstate = true;

        /* -reimport-utxos: force re-import from LevelDB chainstate */
        if (ctx->reimport_utxos) {
            if (!utxo_recovery_prepare_reimport(&g_node_db).ok)
                ctx->reimport_utxos = false;
        }

        /* LDB UTXO import deferred to post-block-index (see below). */
    }

    /* coins_kv-backed read authority: the coins_tip RAM cache resolves
     * misses against coins_kv (canonical UTXO set in progress.kv), authored
     * in-txn by the reducer so it is atomically consistent with the stage
     * cursor on every crash — the durability the projection's separate WAL
     * lacked (tip-wedge tear class, docs/work/tip-durability-collapse.md).
     * We still open the log + UTXO projection (no-op reuse of the later
     * boot_start_projection_storage open): it is the SEED conduit only
     * (coins_kv_boot_rebuild_if_needed copies it into coins_kv); the read view
     * no longer binds it. FATAL only if progress.kv (the coins_kv home) is not
     * open — coins_view_kv binds progress_store_db() lazily at read time. */
    (void)boot_ensure_log_and_utxo_projection(ctx->datadir);
    if (!progress_store_db() ||
        !coins_view_kv_init(&g_coins_read_view)) {
        fprintf(stderr,
                "FATAL: progress.kv (coins_kv) not open; cannot serve "
                "coins reads\n");
        event_emitf(EV_BOOT_VALIDATION_FAILED, 0,
                    "coins_kv unavailable for coins read view");
        _exit(EXIT_FAILURE);
    }
    coins_view_cache_init(&g_coins_tip, &g_coins_read_view.view);

    /* Hoist the block_index_projection open next to the log/utxo projection
     * so load_block_index_from_projection (under -rebuildfromlog) has the
     * caught-up projection available BEFORE the block-index load below.
     * The phase-4 fan-out re-call in
     * boot_start_projection_storage is a no-op reuse (first opener wins).
     * Non-fatal if it cannot open: -rebuildfromlog simply falls through to
     * the legacy loaders. */
    (void)boot_ensure_block_index_projection(ctx->datadir);

    /* Wire the process-lifetime chain_state_repository singleton now
     * that g_coins_tip is alive. From this point on, call-site
     * migrations can go through csr_commit_tip() and get all six
     * sources of truth updated atomically under one mutex. Wallet
     * scan height is unwired (NULL) — the wallet manages its own
     * scan state and must not be driven through the repository. */
    csr_init(csr_instance(),
             &g_state.map_block_index,
             &g_state.chain_active,
             &g_state.pindex_best_header,
             &g_coins_tip,
             &g_node_db,
             NULL);
    csr_set_db_service(csr_instance(), &g_db_service);

    /* Wire UTXO commitment: load from SQLite and set pointer for
     * persistence on flush. */
    set_coins_sqlite_for_commitment(&g_coins_sqlite);
    if (coins_view_sqlite_read_commitment(&g_coins_sqlite, &g_coins_tip.commitment)) {
        printf("Loaded UTXO commitment from SQLite (count=%llu)\n",
               (unsigned long long)g_coins_tip.commitment.count);
    }

    /* skip_activate removed — activation controller is the authority */
    bool fast_restart = false;
    /* Set true if the block index + tip were rebuilt purely from the
     * log-derived projection (-rebuildfromlog). Function-scoped so the
     * legacy UTXO importer block below can also be skipped. */
    bool rebuilt_from_log = false;
    bool boot_restored_authority_tip = false;
    int boot_restored_authority_height = -1;
    struct uint256 boot_restored_authority_hash;
    memset(&boot_restored_authority_hash, 0, sizeof(boot_restored_authority_hash));

    /* Block index is now cached in SQLite (load_block_index_sqlite).
     * The full index is saved on shutdown/save, enabling instant restart
     * without the 10-15s LevelDB scan. */

    /* OOM protection: estimate block index memory before loading.
     * Warn if it would exceed 50% of system RAM. */
    {
        size_t sys_ram = get_system_ram();
        if (sys_ram > 0) {
            /* Estimate from SQLite or flat file entry count */
            int64_t est_count = 0;
            if (g_node_db.open)
                est_count = db_block_max_height(&g_node_db);
            if (est_count <= 0)
                est_count = 3000000; /* conservative default */
            size_t est_mem = (size_t)est_count * sizeof(struct block_index)
                           + (size_t)est_count * 2 * sizeof(struct block_map_entry);
            if (est_mem > sys_ram / 2) {
                fprintf(stderr,
                    "[boot] WARNING: block index estimated at %zuMB "
                    "(%lld entries x %zu bytes + hash map)\n"
                    "[boot] System has %zuMB RAM. This may cause OOM.\n",
                    est_mem / (1024 * 1024), (long long)est_count,
                    sizeof(struct block_index),
                    sys_ram / (1024 * 1024));
            }
            printf("[boot] system_ram=%zuMB block_index_estimate=%zuMB "
                   "(%lld entries)\n",
                   sys_ram / (1024 * 1024), est_mem / (1024 * 1024),
                   (long long)est_count);
        }
    }

    /* Block index load: flat file first (mmap, <2s), then SQLite, then LevelDB.
     * Jeff Dean rule: use the fastest data structure available. */
    t_phase = boot_clock_ms();
    {
        bool loaded = false;

        /* Event-log cold-start (-rebuildfromlog): rebuild the in-memory
         * block index + active tip purely from the log-derived projection,
         * so boot no longer reads the legacy flat/SQLite/LevelDB loaders, the
         * zclassicd-LDB import, or the legacy UTXO importer. The projection
         * read-view (set above) is already the coins authority. Opt-in: only
         * taken when ctx->boot_from_log is set AND the rebuild yields a
         * non-trivial map with an authority tip — otherwise it falls through
         * to the legacy loaders so a sparse/empty projection never bricks
         * boot. fast-sync (snapshot_apply) seeds the projection+cursor on the
         * FRESH path; a warm boot of that node rebuilds here. */
        if (ctx->boot_from_log &&
            boot_try_rebuild_block_index_from_projection(
                &g_state, params, 1000, /*publish_tip=*/true)) {
            rebuilt_from_log = true;
            loaded = true;
        }

        if (!rebuilt_from_log)
            loaded = load_block_index_flat(ctx->datadir, &g_state);
        if (!rebuilt_from_log && !loaded && g_node_db.open)
            loaded = load_block_index_sqlite(&g_node_db, &g_state);

        /* kill-9 recovery: a node SIGKILL'd with no clean shutdown never wrote
         * the flat file (clean-shutdown only) or the >1000-gated block_index
         * cache, so the legacy loaders above yield an empty/genesis-only map
         * and the forward-only finalized-tip seed (boot_services.c) can't
         * resolve its tip_hash. Rebuild the map from the durable per-block
         * block_index_projection (WAL-crash-safe). publish_tip=false → PURE map
         * rebuild, NO early tip: the coins/UTXO authority owns the tip and the
         * GUARDED seed advances forward. Fires ONLY when the legacy loaders
         * came back empty (map.size<=1) — on a real multi-million-entry boot it
         * is never reached, so mainnet behavior is unaffected. Precondition on this
         * path: node.db db_height<=0 (the small chain never flushed it), so the
         * stale-flat (1940) + tip-hash (1957) guards below short-circuit. */
        if (!rebuilt_from_log && g_state.map_block_index.size <= 1 &&
            boot_try_rebuild_block_index_from_projection(
                &g_state, params, 1, /*publish_tip=*/false))
            loaded = true;

        /* Check if flat file is stale — if it loaded but has far fewer
         * entries than the chain (checked via SQLite), reload from LevelDB.
         * This fixes the case where an old flat file with 6K entries
         * prevents loading the full 3M+ entry index.
         * Skipped on the log-rebuild path: the legacy SQLite db_height is
         * not the authority there (the log/cursor tip is), and comparing
         * against it would spuriously discard the valid log-rebuilt map. */
        if (!rebuilt_from_log && loaded && g_node_db.open) {
            int64_t db_height = node_db_sync_get_tip_height(&g_node_db);
            if (db_height < 0)
                db_height = db_block_max_height(&g_node_db);
            size_t flat_count = g_state.map_block_index.size;
            if (db_height > 0 && (int64_t)flat_count < db_height - 1000) {
                printf("Block index flat: stale (%zu entries vs chain height %lld)"
                       " — reloading from LevelDB\n",
                       flat_count, (long long)db_height);
                fflush(stdout);
                loaded = false;  /* fall through to LevelDB */
            }

            /* Consistency check: the persisted sync projection cursor must exist in
             * the loaded flat block index AT THE CORRECT HEIGHT. Use
             * node_state sync_projection_tip_hash/height first: the blocks table can
             * lag by a block after crash recovery or catchup flushing. */
            if (loaded && db_height > 0) {
                uint8_t tip_hash_raw[32];
                bool have_tip_hash =
                    node_db_sync_get_tip_hash(&g_node_db, tip_hash_raw);
                if (have_tip_hash) {
                    struct uint256 tip_hash;
                    memcpy(tip_hash.data, tip_hash_raw, 32);
                    struct block_index *flat_tip = block_map_find(
                        &g_state.map_block_index, &tip_hash);
                    if (!flat_tip ||
                        (int64_t)flat_tip->nHeight != db_height) {
                        fprintf(stderr,
                            "Block index flat: tip hash maps to wrong "
                            "height (%d vs SQLite %lld). Corrupt flat "
                            "file — reloading from SQLite.\n",
                            flat_tip ? flat_tip->nHeight : -1,
                            (long long)db_height);
                        loaded = false;
                    }
                } else {
                    struct db_block tip_blk;
                    if (db_block_find_by_height(&g_node_db, (int)db_height,
                                                 &tip_blk)) {
                        struct uint256 tip_hash;
                        memcpy(tip_hash.data, tip_blk.hash, 32);
                        struct block_index *flat_tip = block_map_find(
                            &g_state.map_block_index, &tip_hash);
                        if (!flat_tip ||
                            (int64_t)flat_tip->nHeight != db_height) {
                            fprintf(stderr,
                                "Block index flat: tip hash maps to wrong "
                                "height (%d vs SQLite %lld). Corrupt flat "
                                "file — reloading from SQLite.\n",
                                flat_tip ? flat_tip->nHeight : -1,
                                (long long)db_height);
                            loaded = false;
                        }
                    }
                }
            }
        }

        if (!rebuilt_from_log && !loaded) {
            int64_t t_idx_start = (int64_t)platform_time_wall_time_t();
            printf("Loading block index from LevelDB...\n");
            if (!load_block_index(&g_state, params, &g_block_tree, g_block_tree_open).ok) {
                fprintf(stderr, "Warning: Failed to load block index\n");
            }
            int64_t t_idx_elapsed = (int64_t)platform_time_wall_time_t() - t_idx_start;
            printf("Block index loaded: %zu entries in %llds\n",
                   g_state.map_block_index.size, (long long)t_idx_elapsed);
            event_emitf(EV_BOOT_BLOCK_INDEX, 0, "loaded entries=%zu elapsed=%llds",
                        g_state.map_block_index.size, (long long)t_idx_elapsed);

            /* Save flat file for next restart */
            if (g_state.map_block_index.size > 1000)
                save_block_index_flat(ctx->datadir, &g_state);
        }

        /* If block index is much smaller than the chain, try loading
         * from zclassicd's LevelDB. This gives us 3M+ entries with
         * correct heights and pprev chains in seconds. Triggers when
         * we have <10% of expected entries (e.g., 3K vs 3M chain).
         * Skipped on the log-rebuild path: the log/projection is the sole
         * authority there, so no legacy LevelDB read is performed. */
        if (!rebuilt_from_log && !ctx->no_legacy_auto_import) {
            int chain_h = active_chain_height(&g_state.chain_active);
            if (chain_h < 1000) {
                /* Estimate expected height from SQLite or coins */
                int64_t db_h = g_node_db.open ? db_block_max_height(&g_node_db) : 0;
                if (db_h > chain_h) chain_h = (int)db_h;
            }
            /* Always try zclassicd LDB if our index has far fewer entries
             * than the chain height. After snapshot sync, our own LDB has
             * only a handful of entries with scrambled heights. */
            bool need_zcd = ((int64_t)g_state.map_block_index.size <
                             (int64_t)chain_h * 9 / 10);
        if (need_zcd) {
            const char *home = getenv("HOME");
            char zcd_idx_path[1024];
            if (home)
                snprintf(zcd_idx_path, sizeof(zcd_idx_path),
                         "%s/.zclassic/blocks/index", home);
            else
                snprintf(zcd_idx_path, sizeof(zcd_idx_path),
                         ".zclassic/blocks/index");

            struct stat zcd_st;
            if (stat(zcd_idx_path, &zcd_st) == 0) {
                printf("Loading block index from zclassicd LevelDB: %s\n",
                       zcd_idx_path);
                fflush(stdout);

                /* Build a snapshot dir so we can open the LevelDB even
                 * while a live zclassicd holds the source LOCK.
                 * Hardlinks the immutable .ldb SST files + copies the
                 * small MANIFEST/CURRENT/LOG metadata + gives the
                 * snapshot a fresh empty LOCK (distinct fcntl context).
                 * See lib/storage/src/ldb_snapshot.c for the rationale. */
                char snap_path[1200];
                snprintf(snap_path, sizeof(snap_path),
                         "%s/.legacy_ldb_snap", ctx->datadir);
                char snap_err[256] = {0};
                bool snap_ok = false;
                for (int snap_try = 0; snap_try < 3 && !snap_ok; snap_try++) {
                    snap_ok = ldb_snapshot_make(zcd_idx_path, snap_path,
                                                snap_err, sizeof(snap_err));
                    if (!snap_ok &&
                        strcmp(snap_err, "manifest_changed") != 0)
                        break;
                }
                const char *open_path = snap_ok ? snap_path : zcd_idx_path;
                if (!snap_ok) {
                    fprintf(stderr,
                            "[boot] ldb_snapshot_make(%s) failed: %s; "
                            "falling back to direct open (may unlink "
                            "stale LOCK)\n", zcd_idx_path, snap_err);
                    /* Fallback path for a crashed zclassicd whose LOCK
                     * is stale: unlink it and open directly. Only used
                     * when the snapshot path itself fails. */
                    char lock_path[1300];
                    snprintf(lock_path, sizeof(lock_path),
                             "%s/LOCK", zcd_idx_path);
                    unlink(lock_path);
                }

                struct block_tree_db zcd_btdb;
                int64_t t0 = (int64_t)platform_time_wall_time_t();
                if (block_tree_db_open(&zcd_btdb, open_path,
                                       450 << 20, false, false)) {
                    if (block_tree_db_load_block_index_guts(
                            &zcd_btdb, boot_insert_block_index_cb, &g_state)) {
                        int64_t elapsed = (int64_t)platform_time_wall_time_t() - t0;
                        printf("Loaded %zu block index entries from zclassicd "
                               "in %llds\n",
                               g_state.map_block_index.size,
                               (long long)elapsed);

                        /* Option A: re-seed per-node hash storage and point
                         * phashBlock at it (boot_insert_block_index_cb ->
                         * chainstate_insert_block_index already does this at
                         * insert; idempotent re-assert here). Never points
                         * into the reallocatable bucket array. */
                        size_t iter2 = 0;
                        struct block_index *pi2;
                        const struct uint256 *hash2;
                        while (block_map_next(&g_state.map_block_index,
                                              &iter2, &hash2, &pi2))
                            if (pi2 && hash2) {
                                pi2->hashBlock = *hash2;
                                pi2->phashBlock = &pi2->hashBlock;
                            }

                        /* Compute chain work + set chain tip directly.
                         * This avoids the O(n^2) find_most_work_chain scan
                         * which is catastrophically slow with 3M entries. */
                        {
                            size_t n = g_state.map_block_index.size;
                            struct block_index **sorted = zcl_malloc(
                                n * sizeof(struct block_index *), "boot.chainwork_sorted");
                            if (sorted) {
                                size_t si = 0, idx2 = 0;
                                struct block_index *sp;
                                while (block_map_next(&g_state.map_block_index,
                                                      &si, NULL, &sp))
                                    if (sp && idx2 < n) sorted[idx2++] = sp;
                                n = idx2;
                                qsort(sorted, n, sizeof(*sorted),
                                      cmp_block_index_height);

                                /* Forward pass: compute nChainWork + nChainTx */
                                struct block_index *best = NULL;
                                for (size_t i = 0; i < n; i++) {
                                    struct block_index *b = sorted[i];
                                    struct arith_uint256 proof = GetBlockProof(b);
                                    if (b->pprev)
                                        arith_uint256_add(&b->nChainWork,
                                            &b->pprev->nChainWork, &proof);
                                    else
                                        b->nChainWork = proof;

                                    if (b->nTx > 0) {
                                        if (b->pprev && b->pprev->nChainTx > 0)
                                            b->nChainTx = b->pprev->nChainTx + b->nTx;
                                        else if (!b->pprev)
                                            b->nChainTx = b->nTx;
                                    }

                                    /* Track best valid chain tip */
                                    if (b->nChainTx > 0 &&
                                        (b->nStatus & BLOCK_HAVE_DATA) &&
                                        !(b->nStatus & BLOCK_FAILED_MASK)) {
                                        if (!best || arith_uint256_compare(
                                                &b->nChainWork,
                                                &best->nChainWork) > 0)
                                            best = b;
                                    }
                                }
                                free(sorted);

                                if (best && best->nHeight > 0 &&
                                    boot_promote_tip_via_csr(
                                        best, "zclassicd_import_best",
                                        false)) {
                                    printf("Chain tip from zclassicd: height=%d "
                                           "nChainTx=%u\n",
                                           best->nHeight, best->nChainTx);
                                }
                            }
                        }

                        /* Save flat file for instant future boots */
                        save_block_index_flat(ctx->datadir, &g_state);
                    }
                    block_tree_db_close(&zcd_btdb);
                } else {
                    fprintf(stderr, "Could not open zclassicd block index "
                            "at %s\n", open_path);
                }
                /* Tear down the snapshot (hardlinks free cheaply). */
                if (snap_ok)
                    ldb_snapshot_destroy(snap_path);

                /* Copy block files from zclassicd if we don't have them */
                if (g_state.map_block_index.size > 1000) {
                    char zcd_blk_dir[1024], c23_blk_dir[1024];
                    if (home)
                        snprintf(zcd_blk_dir, sizeof(zcd_blk_dir),
                                 "%s/.zclassic/blocks", home);
                    else
                        snprintf(zcd_blk_dir, sizeof(zcd_blk_dir),
                                 ".zclassic/blocks");
                    snprintf(c23_blk_dir, sizeof(c23_blk_dir),
                             "%s/blocks", ctx->datadir);

                    for (int fi = 0; fi < 256; fi++) {
                        char src_path[1200], dst_path[1200];
                        snprintf(src_path, sizeof(src_path),
                                 "%s/blk%05d.dat", zcd_blk_dir, fi);
                        snprintf(dst_path, sizeof(dst_path),
                                 "%s/blk%05d.dat", c23_blk_dir, fi);
                        struct stat src_st, dst_st;
                        if (stat(src_path, &src_st) != 0) {
                            if (fi > 2) break; /* stop after gap */
                            continue;
                        }
                        /* Skip if destination already exists and is same size */
                        if (stat(dst_path, &dst_st) == 0 &&
                            dst_st.st_size == src_st.st_size)
                            continue;
                        /* Hard link first (instant, same filesystem) */
                        if (link(src_path, dst_path) == 0) {
                            if (fi % 10 == 0)
                                printf("  linked blk%05d.dat (%lld MB)\n",
                                       fi, (long long)(src_st.st_size >> 20));
                        } else {
                            /* Different filesystem — fall back to copy */
                            char cmd[2500];
                            snprintf(cmd, sizeof(cmd), "cp '%s' '%s'",
                                     src_path, dst_path);
                            printf("  copying blk%05d.dat (%lld MB)...\n",
                                   fi, (long long)(src_st.st_size >> 20));
                            fflush(stdout);
                            system(cmd);
                        }
                        /* Also link/copy rev (undo) files */
                        snprintf(src_path, sizeof(src_path),
                                 "%s/rev%05d.dat", zcd_blk_dir, fi);
                        snprintf(dst_path, sizeof(dst_path),
                                 "%s/rev%05d.dat", c23_blk_dir, fi);
                        if (stat(src_path, &src_st) == 0) {
                            if (link(src_path, dst_path) != 0) {
                                char cmd[2500];
                                snprintf(cmd, sizeof(cmd), "cp '%s' '%s'",
                                         src_path, dst_path);
                                system(cmd);
                            }
                        }
                    }
                    printf("Block files linked/copied from zclassicd\n");
                    fflush(stdout);
                }
            }
        } /* need_zcd */
        } /* chain height check scope */

        /* Save recent blocks to SQLite (skip for large indexes —
         * the flat file handles 3M+ entries in 1-3s and the SQLite
         * cache path uses 10GB+ RAM causing OOM kills) */
        if (g_node_db.open && g_state.map_block_index.size > 1000
            && g_state.map_block_index.size < 500000)
            save_block_index_recent(&g_node_db, &g_state);

        /* Ensure block files from zclassicd are available.
         * Hard-link (instant, same FS) or skip (cross-FS handled above).
         * This runs every boot to catch the case where block_index.bin
         * was loaded from a previous session but blocks/ was wiped. */
        if (!ctx->no_legacy_auto_import &&
            g_state.map_block_index.size > 1000) {
            const char *home = getenv("HOME");
            char zcd_blk[1024], c23_blk[1024];
            if (home)
                snprintf(zcd_blk, sizeof(zcd_blk), "%s/.zclassic/blocks", home);
            else
                snprintf(zcd_blk, sizeof(zcd_blk), ".zclassic/blocks");
            snprintf(c23_blk, sizeof(c23_blk), "%s/blocks", ctx->datadir);

            int linked = 0;
            for (int fi = 0; fi < 256; fi++) {
                char src[1200], dst[1200];
                snprintf(src, sizeof(src), "%s/blk%05d.dat", zcd_blk, fi);
                snprintf(dst, sizeof(dst), "%s/blk%05d.dat", c23_blk, fi);
                struct stat ss, ds;
                if (stat(src, &ss) != 0) { if (fi > 2) break; continue; }
                if (stat(dst, &ds) == 0) continue; /* already exists */
                if (link(src, dst) == 0) linked++;
                /* Also link rev file */
                snprintf(src, sizeof(src), "%s/rev%05d.dat", zcd_blk, fi);
                snprintf(dst, sizeof(dst), "%s/rev%05d.dat", c23_blk, fi);
                if (stat(src, &ss) == 0 && stat(dst, &ds) != 0)
                    link(src, dst);
            }
            if (linked > 0)
                printf("Linked %d block files from zclassicd\n", linked);
        }

        /* Projection top-up (defect #10, task #29): the loaders above only
         * know what was persisted at the LAST boot-time save — every block
         * connected since then (reducer ingest sets HAVE_DATA + positions +
         * nTx in memory only) exists durably ONLY in the event log →
         * block_index_projection. Fold it over the loaded map raise-only so
         * a restart keeps the connected extent instead of dropping to the
         * stale flat-file floor and re-chasing the window. Must run BEFORE
         * the nChainTx propagation below, which turns the topped-up nTx
         * into the connected chain the boot scan keys on. Skipped on the
         * -rebuildfromlog path (the map IS the projection there). */
        if (!rebuilt_from_log &&
            !block_index_projection_topup(&g_state, ctx->datadir)) {
            /* Non-fatal by design (the node re-syncs the lost window from
             * peers), but a silent false here IS the defect-#10 regression
             * — the restart drops to the stale flat-file floor. Say so. */
            LOG_WARN("boot",
                     "[boot] block_index projection top-up FAILED — the "
                     "connected extent may regress to the last flat-file "
                     "save; expect a window re-chase (see node.log lines "
                     "above for the failing fold step)");
        }

        /* Propagate nChainTx for all blocks in the index.
         * The flat file and LevelDB don't always store correct nChainTx.
         * Without this, find_most_work_chain() skips blocks with
         * nChainTx=0, causing "tip=X most_work=Y" with Y << X.
         *
         * Skip-guard: block_index_loader persists+restores nChainTx, so a
         * warm boot loads a map that is ALREADY fully propagated. The
         * malloc+qsort+multi-pass below is O(n log n) over millions of
         * entries while holding boot — wasted work when nothing changed.
         * An O(n) pre-scan confirms every txn-bearing block already carries
         * a nonzero nChainTx (and, where its parent's nChainTx is known, the
         * child equals parent + nTx). If so, skip the rebuild entirely; only
         * the cold/torn-import case (missing nChainTx) pays the full pass. */
        bool nchaintx_already_computed = false;
        if (g_state.map_block_index.size > 100) {
            nchaintx_already_computed = true;
            size_t it = 0;
            struct block_index *bi = NULL;
            while (block_map_next(&g_state.map_block_index, &it, NULL, &bi)) {
                if (!bi)
                    continue;
                if (bi->nTx > 0 && bi->nChainTx == 0) {
                    nchaintx_already_computed = false;
                    break;
                }
                /* pprev-consistency where the parent total is known. */
                if (bi->nHeight > 0 && bi->pprev &&
                    bi->pprev->nChainTx > 0 && bi->nTx > 0 &&
                    bi->nChainTx != bi->pprev->nChainTx + bi->nTx) {
                    nchaintx_already_computed = false;
                    break;
                }
            }
        }
        if (nchaintx_already_computed) {
            printf("nChainTx already computed, skipping propagation\n");
        } else if (g_state.map_block_index.size > 100) {
            size_t n = g_state.map_block_index.size;
            struct block_index **sorted = zcl_malloc(n * sizeof(*sorted), "boot.nchaintx_sorted");
            if (sorted) {
                size_t si = 0, idx = 0;
                struct block_index *sp;
                while (block_map_next(&g_state.map_block_index, &si, NULL, &sp))
                    if (sp && idx < n) sorted[idx++] = sp;
                n = idx;
                /* Sort by height for forward propagation */
                qsort(sorted, n, sizeof(*sorted), cmp_block_index_height);
                /* Multi-pass propagation (converges in 1-2 passes for
                 * a well-connected chain, up to 5 for edge cases) */
                int total = 0;
                for (int pass = 0; pass < 5; pass++) {
                    int propagated = 0;
                    for (size_t i = 0; i < n; i++) {
                        struct block_index *b = sorted[i];
                        if (b->nHeight == 0) {
                            if (b->nChainTx == 0 && b->nTx > 0) {
                                b->nChainTx = b->nTx;
                                propagated++;
                            }
                        } else if (b->pprev && b->pprev->nChainTx > 0 && b->nTx > 0) {
                            unsigned int expected = b->pprev->nChainTx + b->nTx;
                            if (b->nChainTx != expected) {
                                b->nChainTx = expected;
                                propagated++;
                            }
                        }
                    }
                    total += propagated;
                    if (propagated == 0) break;
                }
                free(sorted);
                if (total > 0)
                    printf("nChainTx propagated for %d blocks\n", total);
            }
        }
    }

    printf("[boot] %-30s %lldms\n", "block_index_load",
           (long long)(boot_clock_ms() - t_phase));

    /* Log block index memory usage */
    {
        size_t entry_count = g_state.map_block_index.size;
        size_t entry_bytes = entry_count * sizeof(struct block_index);
        size_t map_bytes = g_state.map_block_index.capacity *
                           sizeof(struct block_map_entry);
        size_t total_bytes = entry_bytes + map_bytes;
        printf("[boot] block_index: %zu entries, %zu bytes/entry, "
               "index=%zuMB map=%zuMB total=%zuMB\n",
               entry_count, sizeof(struct block_index),
               entry_bytes / (1024 * 1024),
               map_bytes / (1024 * 1024),
               total_bytes / (1024 * 1024));
    }

    /* Bulk height repair: fix scrambled nHeight values from LDB import.
     * This must run AFTER block index is loaded but BEFORE header sync.
     * Without this, header processing fixes heights 160-at-a-time which
     * is far too slow for 3M+ entries with wrong heights. */
    int index_repaired = 0;
    if (g_state.map_block_index.size > 100)
        index_repaired += block_index_repair_heights(&g_state);

    /* pprev chain repair: fix corrupted pprev pointers from LDB import.
     * Reads hashPrevBlock from block data on disk and corrects pprev.
     * Must run after height repair (needs correct heights for sort). */
    if (g_state.map_block_index.size > 100) {
        int pprev_fixed = block_index_repair_pprev(&g_state, ctx->datadir);
        if (pprev_fixed > 0) {
            index_repaired += pprev_fixed;
            index_repaired += block_index_repair_heights(&g_state);
        }
    }

    if (index_repaired > 0 && g_state.map_block_index.size > 1000) {
        printf("Block index repaired: saving canonical flat file "
               "(%d repairs)\n", index_repaired);
        save_block_index_flat(ctx->datadir, &g_state);
    }

    /* Block index integrity — verify sidecar SHA3 after all loads.
     *
     * File integrity failures are still quarantined before boot can
     * continue. SQLite cross-check mismatches are different: the loaded
     * block index may be ahead of the SQL metadata during legacy/body-pull
     * recovery, and refusing boot there forces operators into the unsafe
     * ZCL_ALLOW_CORRUPT_INDEX path. Record the mismatch as degraded
     * reconciliation state and let the guarded boot pipeline repair or
     * fill SQL without publishing evidence by fiat. */
    {
        struct block_index *tip = active_chain_tip(&g_state.chain_active);
        char err[256] = "";
        enum bii_verdict v = bii_verify(ctx->datadir, &g_node_db,
                                         tip, err, sizeof(err));
        if (v == BII_OK) {
            bii_record_recovery_status(v, BII_RECOVERY_ACCEPTED,
                                       tip ? "block index sidecar and SQL tip match"
                                           : "block index sidecar valid; no active tip yet",
                                       false, false);
        } else if (v == BII_SIDECAR_MISSING || v == BII_BODY_MISSING) {
            bii_record_recovery_status(v, BII_RECOVERY_ACCEPTED,
                                       err[0] ? err : "first run or index will be rebuilt",
                                       false, false);
        } else if (tip &&
                   (v == BII_TIP_MISSING_IN_SQL ||
                    v == BII_TIP_HEIGHT_MISMATCH)) {
            bii_record_recovery_status(v, BII_RECOVERY_RECONCILE_REQUIRED,
                                       err[0] ? err : bii_verdict_name(v),
                                       true, false);
            fprintf(stderr,
                    "WARNING: block index integrity: %s "
                    "(continuing in degraded reconcile mode)\n",
                    err[0] ? err : bii_verdict_name(v));
        } else {
            const char *allow = getenv("ZCL_ALLOW_CORRUPT_INDEX");
            if (allow && allow[0] == '1') {
                bii_record_recovery_status(v, BII_RECOVERY_OVERRIDE,
                                           err[0] ? err : bii_verdict_name(v),
                                           true, true);
                fprintf(stderr, "WARNING: block index integrity: %s "
                        "(continuing — ZCL_ALLOW_CORRUPT_INDEX=1)\n", err);
            } else {
                bii_record_recovery_status(v, BII_RECOVERY_QUARANTINED,
                                           err[0] ? err : bii_verdict_name(v),
                                           true, false);
                fprintf(stderr, "FATAL: block index integrity: %s\n"
                        "Set ZCL_ALLOW_CORRUPT_INDEX=1 to override.\n", err);
                bii_quarantine_corrupt(ctx->datadir, v);
                return false;
            }
        }
    }

    /* (wave-2 deletion) boot_resolve_cold_import_pending_anchor removed:
     * its 'cold_import_pending_*' node_state keys had NO writer anywhere in
     * the tree — a dead consumer of a key family nothing produced. */

    /* ── LDB UTXO import (runs AFTER block index load) ──
     * Skipped on the log-rebuild path: the UTXO projection (bound as the
     * coins read view above) is the sole money authority there; reading the
     * legacy ~/.zclassic chainstate LevelDB into coins.db would re-introduce
     * the legacy seed this path exists to eliminate. */
    t_phase = boot_clock_ms();
    if (!rebuilt_from_log && !ctx->no_legacy_auto_import) {
        struct utxo_recovery_ctx uctx = {
            .state = &g_state,
            .coins_sqlite = &g_coins_sqlite,
            .coins_tip = &g_coins_tip,
            .ndb = &g_node_db,
            .datadir = ctx->datadir,
            .params = params,
            .activation_ctl = &g_activation_ctl,
            .db_service = boot_runtime_db_service(),
        };
        struct utxo_import_result ir = utxo_recovery_import_ldb(&uctx);
        if (!ir.status.ok)
            fprintf(stderr, "[boot] UTXO import failed: %s\n",
                    ir.status.message);
        if (ir.skip_activate) {
            if (ir.anchor_reason[0])
                activation_set_anchor_active(&g_activation_ctl,
                                              ir.anchor_reason);
        }
    }

    printf("[boot] %-30s %lldms\n", "utxo_import",
           (long long)(boot_clock_ms() - t_phase));

    /* Timing only (no behavior change): mark the start of the
     * block-index reconcile span — single-pass block-index scan,
     * utxo_recovery_restore_chain_tip, block-index repair/relink,
     * utxo_recovery_execute, and the on-disk block-file scan. This stretch
     * sat between the utxo_import and sapling_tree_load markers and was
     * part of the warm-start unattributed gap. */
    int64_t t_reconcile_blockindex = boot_clock_ms();

    /* Resolve -deferproofvalidationbelow=<hash> now that block index is loaded */
    if (ctx->defer_proof_validation_below && strcmp(ctx->defer_proof_validation_below, "0") != 0) {
        struct uint256 av_hash;
        if (strlen(ctx->defer_proof_validation_below) == 64) {
            /* Parse hex hash (reversed byte order like block explorer) */
            for (int bi = 0; bi < 32; bi++) {
                unsigned int byte;
                sscanf(ctx->defer_proof_validation_below + bi * 2, "%02x", &byte);
                av_hash.data[31 - bi] = (uint8_t)byte;
            }
            struct block_index *pav = block_map_find(&g_state.map_block_index,
                                                      &av_hash);
            if (pav) {
                g_deferred_proof_validation_below_height = pav->nHeight;
                printf("Deferred proof validation: height %d (from hash)\n",
                       g_deferred_proof_validation_below_height);
            } else {
                printf("Deferred proof validation: hash not found in block index, "
                       "using checkpoint default\n");
            }
        } else {
            fprintf(stderr, "Warning: -deferproofvalidationbelow hash must be 64 hex chars\n");
        }
    }

    /* ── Single-pass block index scan ────────────────────────────
     * Previously 6+ separate O(n) scans of 3M entries (15-20s).
     * Now ONE pass that: clears BLOCK_FAILED, finds best header,
     * finds fallback (most chain work with HAVE_DATA+nChainTx),
     * finds reindex target, tracks max HAVE_DATA height. */
    struct block_index *scan_best_header = NULL;  /* most chain work */
    struct block_index *scan_fallback = NULL;      /* most work w/ data */
    struct block_index *scan_reindex_best = NULL;  /* highest w/ pprev+nChainTx */
    int scan_cleared_failed = 0;
    int scan_max_have_data_h = 0;
    int scan_missing_header_data = 0;

    {
        size_t si = 0;
        struct block_index *sp;
        while (block_map_next(&g_state.map_block_index, &si, NULL, &sp)) {
            if (!sp) continue;
            /* Clear BLOCK_FAILED */
            if (sp->nStatus & BLOCK_FAILED_MASK) {
                sp->nStatus &= ~BLOCK_FAILED_MASK;
                scan_cleared_failed++;
            }
            /* Best header (most chain work) */
            if (!scan_best_header ||
                arith_uint256_compare(&sp->nChainWork,
                                      &scan_best_header->nChainWork) > 0)
                scan_best_header = sp;
            /* Fallback: most work with HAVE_DATA + nChainTx */
            if ((sp->nStatus & BLOCK_HAVE_DATA) && sp->nChainTx > 0) {
                if (!scan_fallback ||
                    arith_uint256_compare(&sp->nChainWork,
                                          &scan_fallback->nChainWork) > 0)
                    scan_fallback = sp;
            }
            /* Reindex target: highest with pprev + nChainTx */
            if (sp->pprev && sp->nHeight > 0 && sp->nChainTx > 0) {
                if (!scan_reindex_best ||
                    sp->nHeight > scan_reindex_best->nHeight)
                    scan_reindex_best = sp;
            }
            /* Max HAVE_DATA height */
            if ((sp->nStatus & BLOCK_HAVE_DATA) &&
                sp->nHeight > scan_max_have_data_h)
                scan_max_have_data_h = sp->nHeight;
            if ((sp->nStatus & BLOCK_HAVE_DATA) && sp->nDataPos > 0 &&
                sp->nHeight > 0 &&
                (sp->nVersion == 0 || sp->nTime == 0 || sp->nBits == 0))
                scan_missing_header_data++;
        }
    }
    if (scan_cleared_failed > 0)
        printf("Cleared BLOCK_FAILED from %d block index entries\n",
               scan_cleared_failed);
    if (scan_missing_header_data > 0)
        printf("Block index has %d HAVE_DATA entries with missing headers; "
               "will hydrate from block files\n",
               scan_missing_header_data);

    /* Restore chain tip from coins DB best block hash */
    if (ctx->reindex_chainstate) {
        if (scan_reindex_best) {
            if (boot_promote_tip_via_csr(scan_reindex_best,
                                         "scan_reindex_best", false)) {
                printf("Reindex target: height=%d\n",
                       scan_reindex_best->nHeight);
            }
        } else {
            printf("Reindex: no best found (total=%zu)\n",
                   g_state.map_block_index.size);
        }
        if (!reindex_chainstate(&g_state, &g_coins_sqlite, &g_coins_tip,
                                 &g_node_db, ctx->datadir)) {
            fprintf(stderr, "Warning: Chainstate reindex had errors\n");
        }
    } else if (fast_restart) {
    } else if (g_state.map_block_index.size > 1) {
        struct utxo_recovery_ctx uctx = {
            .state = &g_state,
            .coins_sqlite = &g_coins_sqlite,
            .coins_tip = &g_coins_tip,
            .ndb = &g_node_db,
            .datadir = ctx->datadir,
            .params = params,
            .activation_ctl = &g_activation_ctl,
            .db_service = boot_runtime_db_service(),
        };
        struct chain_restore_result cr =
            utxo_recovery_restore_chain_tip(&uctx, scan_fallback);
        if (!cr.status.ok)
            fprintf(stderr, "[boot] UTXO chain restore failed: %s\n",
                    cr.status.message);
        if (cr.restored && cr.restored_height > 0) {
            boot_restored_authority_tip = true;
            boot_restored_authority_height = cr.restored_height;
            boot_restored_authority_hash = cr.restored_hash;

            if (!active_chain_tip(&g_state.chain_active) &&
                !uint256_is_null(&boot_restored_authority_hash)) {
                struct block_index *restored = block_map_find(
                    &g_state.map_block_index, &boot_restored_authority_hash);
                /* HEIGHT-AGREEMENT BELT (Invariant A consumer side): the
                 * restore result's hash must map to an index block AT the
                 * recorded height before it may become the live tip. The
                 * 2026-06-11 wedge installed h=3,143,175 here from a floor
                 * row recorded at 3,143,171 — a raw install of fabricated
                 * state that ended in crash-only reindex. */
                if (restored && restored->nHeight == cr.restored_height) {
                    int populated = chain_restore_rebuild_active_chain(
                        &g_state, restored, NULL);
                    fprintf(stderr,
                        "[boot] restored authority tip h=%d had no active "
                        "chain slot after restore; reinstalled populated=%d\n",
                        restored->nHeight, populated);
                } else if (restored) {
                    fprintf(stderr,
                        "[boot] restored authority hash maps to h=%d but the "
                        "recorded restore height is %d — refusing raw tip "
                        "install (hash/height disagreement; waiting for "
                        "P2P)\n",
                        restored->nHeight, cr.restored_height);
                    event_emitf(EV_RECOVERY_ACTION, 0,
                        "action=restore_reinstall_refused mapped_h=%d "
                        "recorded_h=%d", restored->nHeight,
                        cr.restored_height);
                }
            }
        }
        if (cr.skip_activate) {
            if (cr.anchor_reason[0])
                activation_set_anchor_active(&g_activation_ctl,
                                              cr.anchor_reason);
        }
        if (scan_best_header)
            (void)boot_promote_header_via_csr(scan_best_header,
                                              "scan_best_header");
    }

    /* Ensure genesis block is always properly initialized.
     * On a fresh start, load_block_index creates genesis. On restart,
     * LevelDB may have entries but genesis might lack BLOCK_HAVE_DATA
     * or chain_active might not have a tip set. Fix both. */
    {
        struct block_index *genesis = block_map_find(
            &g_state.map_block_index, &params->consensus.hashGenesisBlock);
        if (!genesis) {
            genesis = chainstate_insert_block_index(
                (struct chainstate *)&g_state,
                &params->consensus.hashGenesisBlock);
        }
        if (genesis) {
            if (genesis->nHeight != 0)
                genesis->nHeight = 0;
            if (!(genesis->nStatus & BLOCK_HAVE_DATA)) {
                genesis->nStatus |= BLOCK_HAVE_DATA;
                genesis->nStatus = (genesis->nStatus & ~BLOCK_VALID_MASK) |
                                    BLOCK_VALID_SCRIPTS;
                genesis->nTx = 1;
                genesis->nChainTx = 1;
                printf("Genesis block: marked BLOCK_HAVE_DATA\n");
            }
            if (arith_uint256_is_zero(&genesis->nChainWork))
                genesis->nChainWork = GetBlockProof(genesis);
            /* Set chain tip to genesis on true fresh boots only. If the
             * coins/UTXO authority restored a non-genesis tip, never turn a
             * cache-rebuild failure into a rollback to height 0: that makes
             * valid historical UTXOs look stale and triggers destructive
             * recovery paths. */
            if (!active_chain_tip(&g_state.chain_active)) {
                if (boot_restored_authority_tip) {
                    fprintf(stderr,
                        "[boot] skipped genesis_init: restored authority "
                        "tip h=%d remains authoritative while active_chain "
                        "is being rebuilt\n",
                        boot_restored_authority_height);
                    event_emitf(EV_BOOT_VALIDATION_FAILED, 0,
                                "skip_genesis_init restored_authority_tip=%d",
                                boot_restored_authority_height);
                } else if (boot_promote_tip_via_csr(genesis, "genesis_init",
                                                    false)) {
                    printf("Chain tip: initialized to genesis (height 0)\n");
                }
            }
        }
    }

    /* Repair block index from SQLite.
     * After legacy import, blocks in the LevelDB index may lack BLOCK_VALID_SCRIPTS
     * (they were validated by zclassicd but our index doesn't know that).
     * Without this, reducer activation won't extend the chain past
     * previously-connected blocks because it only follows fully-validated
     * entries. Also fix any stale file positions. */
    if (g_node_db.open && g_state.map_block_index.size > 1000) {
        /* Only repair blocks near the tip (within 1000 of chain height).
         * The flat file load already has correct data for most blocks.
         * Full 3M-row scan was taking 8+ minutes — this takes <100ms. */
        int repair_from = active_chain_height(&g_state.chain_active) - 1000;
        if (repair_from < 0) repair_from = 0;
        sqlite3_stmt *sel = NULL;
        char repair_sql[256];
        snprintf(repair_sql, sizeof(repair_sql),
            "SELECT hash, file_num, data_pos, status FROM blocks "
            "WHERE file_num >= 0 AND data_pos >= 0 AND height >= %d",
            repair_from);
        int rc = sqlite3_prepare_v2(g_node_db.db, repair_sql, -1, &sel, NULL);
        if (rc == SQLITE_OK && sel) {
            int repaired = 0, checked = 0;
            while (sqlite3_step(sel) == SQLITE_ROW) {
                const void *hash_blob = sqlite3_column_blob(sel, 0);
                int hash_len = sqlite3_column_bytes(sel, 0);
                int file_num = sqlite3_column_int(sel, 1);
                int data_pos = sqlite3_column_int(sel, 2);
                int status = sqlite3_column_int(sel, 3);

                if (!hash_blob || hash_len != 32) continue;

                struct uint256 hash;
                memcpy(hash.data, hash_blob, 32);

                struct block_index *bi = block_map_find(
                    &g_state.map_block_index, &hash);
                if (!bi) continue;
                checked++;

                bool changed = false;

                /* Fix file positions */
                if (bi->nFile != file_num || bi->nDataPos != (unsigned)data_pos) {
                    if (file_num >= 0 && data_pos > 0) {
                        bi->nFile = file_num;
                        bi->nDataPos = (unsigned)data_pos;
                        changed = true;
                    }
                }

                /* Promote validation status from SQLite. A prior import
                 * (e.g. -cold-import) may have persisted BLOCK_HAVE_DATA,
                 * BLOCK_HAVE_UNDO, and BLOCK_VALID_SCRIPTS into the
                 * SQLite block_index ahead of this in-memory load. */
                if (status > 0 && (bi->nStatus & BLOCK_VALID_MASK) <
                    (unsigned)(status & BLOCK_VALID_MASK)) {
                    bi->nStatus = (bi->nStatus & ~(unsigned)BLOCK_VALID_MASK) |
                                  ((unsigned)status & (BLOCK_VALID_MASK |
                                   BLOCK_HAVE_DATA | BLOCK_HAVE_UNDO));
                    changed = true;
                }

                if (changed) repaired++;
            }
            sqlite3_finalize(sel);
            if (repaired > 0) {
                printf("Block index repair: updated %d/%d entries from SQLite\n",
                       repaired, checked);
                fflush(stdout);
            }
        }
    }

    /* Option A: phashBlock now references per-node block_index.hashBlock
     * (stable, never freed by bucket realloc), seeded at every insert.
     * The old bucket-identity relink pass is intentionally removed: under
     * Option A 'phashBlock != &bucket.hash' is ALWAYS true, so it would
     * re-point every node BACK into the reallocatable bucket array and
     * re-introduce the UAF. No relink is needed. */

    /* Validate coins/chain agreement and execute recovery */
    {
        struct boot_validation_result vr =
            validate_coins_chain_agreement(&g_state, &g_coins_tip,
                                           ctx->datadir);
        struct utxo_recovery_ctx uctx = {
            .state = &g_state,
            .coins_sqlite = &g_coins_sqlite,
            .coins_tip = &g_coins_tip,
            .ndb = &g_node_db,
            .datadir = ctx->datadir,
            .params = params,
            .activation_ctl = &g_activation_ctl,
            .db_service = boot_runtime_db_service(),
        };
        struct recovery_exec_result rr = utxo_recovery_execute(&uctx, &vr);
        if (!rr.status.ok)
            fprintf(stderr, "[boot] UTXO recovery execution failed: %s\n",
                    rr.status.message);
        (void)rr.skip_activate; /* activation controller handles state */

        /* Enter turbo mode if genesis reset happened */
        if (rr.recovered &&
            (vr.action == BOOT_RECOVER_REIMPORT ||
             vr.action == BOOT_RECOVER_WIPE_WAIT) &&
            g_node_db.open) {
            if (!boot_db_enter_turbo_mode())
                fprintf(stderr, "boot: failed to enter turbo mode\n");
            if (!boot_db_set_sync_batch_size(1000))
                fprintf(stderr, "boot: failed to set sync batch size\n");
        }
    }

    /* Clear stale HAVE_DATA above tip — targeted, not full scan.
     * Only needed if max HAVE_DATA height > chain tip (from the
     * single-pass scan above). Skip when block index has 1M+ entries
     * — that means it was loaded from zclassicd's LDB with correct
     * nFile/nDataPos, and clearing HAVE_DATA would force re-download
     * of 3M blocks that are already on disk. */
    {
        int tip_h = active_chain_height(&g_state.chain_active);
        if (scan_max_have_data_h > tip_h && tip_h > 0 &&
            g_state.map_block_index.size < 1000000) {
            int cleared = 0;
            size_t ci = 0;
            struct block_index *cp;
            while (block_map_next(&g_state.map_block_index, &ci, NULL, &cp)) {
                if (cp && cp->nHeight > tip_h &&
                    (cp->nStatus & BLOCK_HAVE_DATA)) {
                    cp->nStatus &= ~BLOCK_HAVE_DATA;
                    cleared++;
                }
            }
            if (cleared > 0)
                printf("Cleared stale HAVE_DATA from %d blocks above tip %d\n",
                       cleared, tip_h);
        }
    }

    /* Scan block files on disk if HAVE_DATA is missing or if entries claim
     * HAVE_DATA but still have placeholder header fields. The latter blocks
     * activation because the validator must reject nBits=0 placeholders.
     * Uses scan_max_have_data_h from the single-pass scan above
     * instead of another partial iteration. */
    {
        bool need_scan = (scan_max_have_data_h < 100 &&
                          g_state.map_block_index.size > 1000) ||
                         g_state.map_block_index.size < 100 ||
                         scan_missing_header_data > 0;
        if (need_scan) {
            bool have_block_files = false;
            for (int ci = 0; ci < 3 && !have_block_files; ci++) {
                char check_path[576];
                snprintf(check_path, sizeof(check_path),
                         "%s/blocks/blk%05d.dat", ctx->datadir, ci);
                struct stat check_st;
                if (stat(check_path, &check_st) == 0 && check_st.st_size > 0)
                    have_block_files = true;
            }
            if (have_block_files) {
                scan_block_files_mark_data(&g_state, ctx->datadir, params);
                fflush(stdout);
                if (g_state.map_block_index.size > 1000)
                    save_block_index_flat(ctx->datadir, &g_state);

                /* Wave 2: on canonical datadirs the post-scan anchor ladder
                 * below is GUESSWORK over caches (node_state anchor, mirror
                 * MAX(height), "most work scanned") that manufactured wedges;
                 * the coins-best fact is derived from coins_kv and the
                 * regular restore path (now derivation-gated) installs the
                 * tip. Skip the whole ladder. Legacy datadirs keep it.
                 * (ladder body = wave-3 delete) */
                struct boot_derived_coins_best scan_dcb;
                if (boot_derive_coins_best(&scan_dcb)) {
                    printf("[boot] post-scan anchor ladder skipped — derived "
                           "coins-best h=%d (coins_kv authority)\n",
                           scan_dcb.height);
                } else {
                /* After block file scan, try to resolve coins_best_block.
                 * The scan may have assigned wrong heights (blocks in random
                 * file order) or picked a wrong "most work" chain due to
                 * incomplete nChainTx propagation.  Use SQLite blocks table
                 * to find the correct height, then set the active chain tip
                 * to the coins-tip block.  This fires when:
                 *   (a) active_chain is empty (no HAVE_DATA blocks), OR
                 *   (b) active_chain tip is far below the coins tip
                 *       (scan picked a wrong short fork). */
                struct uint256 post_scan_best;
                /* This LDB/damage-recovery branch needs the LEGACY coins.db
                 * best-block, not the projection (the projection read view
                 * tracks a consume offset, not a best-block hash). Read it
                 * straight from g_coins_sqlite, which still exists for
                 * legacy damage-recovery reads. */
                uint256_set_null(&post_scan_best);
                if (g_coins_sqlite.db)
                    coins_view_sqlite_get_best_block(&g_coins_sqlite,
                                                     &post_scan_best);
                /* Restore chain tip to match UTXO snapshot height when
                 * the active chain is far below the coins tip. This happens
                 * after LDB import: the UTXO set is at 3M+ but block files
                 * only cover up to ~2M, so reducer activation sets a low
                 * tip. Without this fix, the node tries to re-connect
                 * blocks that are already reflected in the UTXO set, causing
                 * bad-txns-inputs-missingorspent failures. */
                if (!uint256_is_null(&post_scan_best)) {
                    /* Look up correct height from SQLite */
                    int target_h = -1;
                    if (g_node_db.open && g_node_db.db) {
                        /* Look up import height from SQLite blocks table.
                         * blocks.hash stores display-order (big-endian),
                         * coins_best_block is internal-order (little-endian). */
                        uint8_t hash_rev[32];
                        for (int bi = 0; bi < 32; bi++)
                            hash_rev[bi] = post_scan_best.data[31 - bi];

                        struct db_block sqlite_blk;
                        if (db_block_find_by_hash(&g_node_db, hash_rev,
                                                   &sqlite_blk) &&
                            sqlite_blk.height > 0) {
                            target_h = sqlite_blk.height;
                        }

                        /* Fallback: try finding by height range near chain tip */
                        if (target_h <= 0) {
                            sqlite3_stmt *qs = NULL;
                            sqlite3_prepare_v2(g_node_db.db,
                                "SELECT height FROM blocks "
                                "ORDER BY height DESC LIMIT 1",
                                -1, &qs, NULL);
                            if (qs) {
                                if (sqlite3_step(qs) == SQLITE_ROW)
                                    target_h = sqlite3_column_int(qs, 0);
                                sqlite3_finalize(qs);
                            }
                            if (target_h > 0)
                                printf("Post-scan: using max block height "
                                       "%d as import target\n", target_h);
                        }

                        if (target_h > 0)
                            printf("Post-scan: import height=%d\n", target_h);
                    }

                    struct block_index *post_found = block_map_find(
                        &g_state.map_block_index, &post_scan_best);
                    if (post_found && target_h > 0) {
                        struct block_index *best_scanned = NULL;
                        size_t scan_iter = 0;
                        struct block_index *scan_bi;
                        while (block_map_next(&g_state.map_block_index,
                                              &scan_iter, NULL, &scan_bi)) {
                            if (!scan_bi) continue;
                            if (!(scan_bi->nStatus & BLOCK_HAVE_DATA)) continue;
                            if (scan_bi->nChainTx == 0) continue;
                            if (!best_scanned ||
                                arith_uint256_compare(&scan_bi->nChainWork,
                                                      &best_scanned->nChainWork) > 0)
                                best_scanned = scan_bi;
                        }
                        if (best_scanned &&
                            best_scanned->nHeight > post_found->nHeight + 1000) {
                            printf("Post-scan: promoting best scanned chain "
                                   "h=%d over stale coins anchor h=%d\n",
                                   best_scanned->nHeight, post_found->nHeight);
                            post_found = best_scanned;
                            target_h = best_scanned->nHeight;
                        }
                        /* SQLite block metadata is a cache and can lag or
                         * carry stale labels after recovery.  The pprev chain
                         * is the authority for block heights here; never
                         * mutate a block_index height from SQLite metadata.
                         * A one-block downlabel is enough to make the active
                         * tip silently disagree with peers and can prune live
                         * UTXOs above the false tip. */
                        if (post_found->nHeight != target_h) {
                            printf("Post-scan: ignoring SQLite height %d for "
                                   "pprev-derived h=%d\n",
                                   target_h, post_found->nHeight);
                            target_h = post_found->nHeight;
                        }
                        if (boot_promote_tip_via_csr(
                                post_found, "post_found_promote", true)) {
                            printf("Post-scan: setting chain tip to h=%d\n",
                                   target_h);
                        }
                    } else if (!post_found) {
                        /* coins_best_block hash not found in block index.
                         * Instead of wiping UTXOs, find the highest UTXO
                         * height and set chain tip there.  The UTXO data
                         * is valid — only the metadata label is wrong. */
                        char hex[65];
                        uint256_get_hex(&post_scan_best, hex);
                        printf("[boot] coins_best_block %s not in "
                               "block index — resolving from UTXO "
                               "heights\n", hex);

                        int utxo_max_h = 0;
                        {
                            sqlite3_stmt *hst = NULL;
                            if (sqlite3_prepare_v2(g_node_db.db,
                                "SELECT MAX(height) FROM utxos",
                                -1, &hst, NULL) == SQLITE_OK && hst) {
                                if (sqlite3_step(hst) == SQLITE_ROW)
                                    utxo_max_h = sqlite3_column_int(hst, 0);
                                sqlite3_finalize(hst);
                            }
                        }

                        if (utxo_max_h > 0) {
                            /* Find highest HAVE_DATA block at or below
                             * the UTXO height — conservative but safe. */
                            struct block_index *best_have = NULL;
                            size_t bi = 0;
                            struct block_index *bp;
                            while (block_map_next(
                                &g_state.map_block_index,
                                &bi, NULL, &bp)) {
                                if (!bp) continue;
                                if (bp->nHeight <= utxo_max_h &&
                                    (bp->nStatus & BLOCK_HAVE_DATA) &&
                                    chain_restore_block_is_consensus_backed_on_disk(
                                        bp, g_datadir) &&
                                    (!best_have ||
                                     bp->nHeight > best_have->nHeight))
                                    best_have = bp;
                            }

                            if (best_have && best_have->nHeight > 0 &&
                                boot_promote_tip_via_csr(
                                    best_have, "coins_hash_orphan_promote",
                                    true)) {
                                printf("[boot] coins_best_block hash not "
                                       "in index — setting tip to highest "
                                       "HAVE_DATA block at h=%d\n",
                                       best_have->nHeight);
                            } else {
                                /* No verified disk-backed blocks — record
                                 * metadata only at the UTXO height. */
                                struct block_index *anchor =
                                    chain_restore_create_anchor(
                                        &g_state, &post_scan_best,
                                        utxo_max_h);
                                if (anchor) {
                                    snapsync_set_anchor(anchor);

                                    printf("[boot] coins_best_block hash "
                                           "not in index — metadata anchor "
                                           "at h=%d\n", utxo_max_h);
                                }
                            }
                        } else {
                            /* No UTXOs at all — safe to reset to genesis */
                            printf("[boot] No UTXOs found — resetting to "
                                   "genesis\n");
                            struct block_index *genesis = block_map_find(
                                &g_state.map_block_index,
                                &params->consensus.hashGenesisBlock);
                            if (genesis) {
                                (void)boot_promote_tip_via_csr(
                                    genesis, "no_utxos_reset_genesis", true);
                            }
                        }
                    }
                }
                }  /* end legacy post-scan ladder (!derived) */
            }
        }
    }

    printf("[boot] %-30s %lldms\n", "block_index_reconcile",
           (long long)(boot_clock_ms() - t_reconcile_blockindex));

    t_phase = boot_clock_ms();
    /* Wire the flat-file sapling checkpoint. Tells
     * process_block.c where to flush every 10K blocks; separate from
     * the node_state-backed path because the flat file is immune to
     * the P14 savepoint contention class. */
    set_sapling_checkpoint_datadir(g_datadir);

    /* Load Sapling commitment tree from persistent storage.
     *
     * Three-tier fall-back, most-authoritative first:
     *   (1) Flat-file checkpoint at <datadir>/sapling_tree_ckpt.dat
     * SHA3-verified, atomic, ≤10K blocks stale.
     *   (2) node_state["sapling_tree"] — SQLite-backed, legacy path,
     *       kept as a secondary belt.
     *   (3) Fresh empty tree + replay during the mismatch-check pass.
     *
     * This tree is maintained by connect_block and verified against
     * hashFinalSaplingRoot in each block header. */
    if (g_node_db.open && !g_state.sapling_tree_loaded && g_datadir) {
        char ckpt_path[512];
        snprintf(ckpt_path, sizeof(ckpt_path),
                 "%s/sapling_tree_ckpt.dat", g_datadir);
        sapling_tree_init(&g_state.sapling_tree);
        int64_t ckpt_height = 0;
        if (sapling_tree_load_checkpoint(&g_state.sapling_tree,
                                          &ckpt_height, ckpt_path)) {
            g_state.sapling_tree_loaded = true;
            set_sapling_tree_for_flush(&g_state.sapling_tree);
            printf("Sapling tree loaded from checkpoint: "
                   "%zu commitments, height=%lld\n",
                   incremental_tree_size(&g_state.sapling_tree),
                   (long long)ckpt_height);
        }
    }

    if (g_node_db.open && !g_state.sapling_tree_loaded) {
        uint8_t tree_buf[8192];
        size_t tree_len = 0;
        if (node_db_state_get(&g_node_db, "sapling_tree",
                               tree_buf, sizeof(tree_buf), &tree_len)
            && tree_len > 0) {
            struct byte_stream ts;
            stream_init_from_data(&ts, tree_buf, tree_len);
            sapling_tree_init(&g_state.sapling_tree);
            if (incremental_tree_deserialize(&g_state.sapling_tree, &ts)) {
                g_state.sapling_tree_loaded = true;
                set_sapling_tree_for_flush(&g_state.sapling_tree);
                printf("Sapling tree loaded: %zu commitments\n",
                       incremental_tree_size(&g_state.sapling_tree));
            } else {
                fprintf(stderr, "WARNING: Sapling tree deserialization "
                        "failed — tree will rebuild during sync\n");
                sapling_tree_init(&g_state.sapling_tree);
            }
        } else {
            printf("No saved Sapling tree — will build during sync\n");
            g_state.sapling_tree_loaded = true; /* empty tree is valid pre-Sapling */
            set_sapling_tree_for_flush(&g_state.sapling_tree);
        }
    }

    /* Verify Sapling tree root matches chain tip. If mismatched,
     * rebuild from block files before P2P starts (no concurrency risk).
     * Skip if hashFinalSaplingRoot is all-zeros (block_index.bin doesn't
     * store this field yet, so it will be zero after flat file load). */
    if (g_state.sapling_tree_loaded && g_datadir) {
        const struct block_index *tip = active_chain_tip(&g_state.chain_active);
        static const uint8_t zeros[32] = {0};
        bool tip_has_sapling_root = tip && tip->nHeight > 476969 &&
            memcmp(tip->hashFinalSaplingRoot.data, zeros, 32) != 0;
        if (tip_has_sapling_root) {
            struct uint256 tree_root;
            incremental_tree_root(&g_state.sapling_tree, &tree_root);
            if (memcmp(tree_root.data,
                       tip->hashFinalSaplingRoot.data, 32) != 0) {
                size_t old_size = incremental_tree_size(&g_state.sapling_tree);
                if (tip->nHeight > 1000000) {
                    printf("Sapling tree root MISMATCH (size=%zu) - "
                           "deferring live rebuild until after boot "
                           "(tip_h=%d)\n", old_size, tip->nHeight);
                    atomic_store(&g_sapling_tree_rebuilding, true);
                    goto sapling_tree_boot_check_done;
                }
                printf("Sapling tree root MISMATCH (size=%zu) — "
                       "rebuilding from block files...\n", old_size);
                fflush(stdout);
                atomic_store(&g_sapling_tree_rebuilding, true);
                int n = sapling_tree_rebuild(&g_node_db,
                    &g_state.chain_active, g_datadir);
                if (n >= 0) {
                    /* Reload the rebuilt tree from node_state */
                    uint8_t tbuf[8192];
                    size_t tlen = 0;
                    if (node_db_state_get(&g_node_db, "sapling_tree",
                            tbuf, sizeof(tbuf), &tlen) && tlen > 0) {
                        struct byte_stream ts2;
                        stream_init_from_data(&ts2, tbuf, tlen);
                        sapling_tree_init(&g_state.sapling_tree);
                        incremental_tree_deserialize(
                            &g_state.sapling_tree, &ts2);
                        set_sapling_tree_for_flush(&g_state.sapling_tree);
                        printf("Sapling tree rebuilt: %d commitments "
                               "(was %zu)\n", n, old_size);
                    }
                }
                atomic_store(&g_sapling_tree_rebuilding, false);
                /* Checkpoint WAL after bulk tree writes */
                node_db_wal_checkpoint(&g_node_db);
                /* Save block_index.bin after rebuild — the entries
                 * now have correct hashFinalSaplingRoot fields from
                 * the rebuild. This prevents needless 5-min rebuilds
                 * on future boots AND ensures coins_best_block will
                 * be resolvable after a crash. */
                save_block_index_flat(ctx->datadir, &g_state);
            }
        }
sapling_tree_boot_check_done:
        ;
    }

    printf("[boot] %-30s %lldms\n", "sapling_tree_load",
           (long long)(boot_clock_ms() - t_phase));

    /* Timing only (no behavior change): mark the start of the
     * UTXO/chain reconcile span — clear-failed-above-tip, the
     * coins-vs-chain height mismatch repair, clean-above-tip, and the
     * activation anchor cleanup. This stretch sat between the
     * sapling_tree_load marker and the reducer activation boot phase and was
     * part of the warm-start unattributed gap. */
    int64_t t_reconcile_utxochain = boot_clock_ms();

    /* Clear BLOCK_FAILED flags above the chain tip on boot.
     * After a UTXO repair or crash recovery, blocks may be marked
     * BLOCK_FAILED_VALID/BLOCK_FAILED_CHILD from a prior session where
     * the UTXO set was incomplete. With the UTXO set now repaired,
     * these blocks should be re-validated. Without this, reducer activation
     * skips them and the node is permanently stuck. */
    chain_restore_clear_failed_above_tip(&g_state);

    /* Safety: verify chain tip matches UTXO set height (coins anchor
     * promotion + coins_best/tip reconcile, every real-block promotion
     * gated by the Invariant A trust-root check). Body lives in
     * boot_index.c. */
    boot_index_verify_coins_tip_consistency(&g_state, &g_coins_sqlite,
                                            &g_node_db);

    /* Clean up UTXOs above chain tip only after the mismatch repair above
     * has had a chance to promote a durable snapshot/coins anchor. Running
     * this earlier can delete the high-water UTXO rows that prove where an
     * immutable historical snapshot actually lands. */
    utxo_recovery_clean_above_tip(&g_node_db, &g_state);

    /* Activate best chain via controller (single authority).
     * The controller checks: anchor state, shutdown, UTXO availability.
     * Replaces the old skip_activate boolean with state machine. */
    if (activation_get_state(&g_activation_ctl) == ACTIVATION_BOOT_PENDING)
        activation_boot_complete(&g_activation_ctl, "boot_done");

    /* If anchor exists but chain tip is already past it (previous boot
     * synced successfully), clear the anchor so blocks can connect. */
    if (activation_get_state(&g_activation_ctl) == ACTIVATION_ANCHOR_ACTIVE) {
        struct block_index *tip = active_chain_tip(&g_state.chain_active);
        struct block_index *anc = snapsync_get_anchor();
        if (!anc) {
            printf("Restore anchor cleared during boot finalize — enabling activation\n");
            activation_clear_anchor(&g_activation_ctl, "restore_anchor_cleared");
        } else if (tip && anc && tip->nHeight > anc->nHeight) {
            printf("Anchor at h=%d below chain tip h=%d — clearing\n",
                   anc->nHeight, tip->nHeight);
            snapsync_set_anchor(NULL);
            activation_clear_anchor(&g_activation_ctl, "tip_past_anchor");
        }
    }
    printf("[boot] %-30s %lldms\n", "utxo_chain_reconcile",
           (long long)(boot_clock_ms() - t_reconcile_utxochain));

    /* Reducer cursor/coins desync reconcile — runs AFTER coins_best is durable
     * (utxo_chain_reconcile above) and BEFORE the staged reducer Jobs init in
     * app_init_services, so the stages load a corrected cursor. If an unclean
     * restart left the tip_finalize cursor AHEAD of the applied coins tip,
     * tip_finalize idles and the connect gate rejects every block as
     * "block-not-finalized-by-reducer", wedging the chain. Clamp tip_finalize
     * down to the served-tip floor (coins_best == the applied tip's OWN height,
     * NOT coins_best+1 — task #31 unifies this with the served-tip cursor
     * convention) so it re-finalizes forward. Reset-safe: deletes no log rows,
     * so the public tip can never drop below coins_best (proven in
     * test_stage_reducer_unwedge). No-op unless the cursor is ahead. */
    {
        /* SINGLE SOURCE OF TRUTH (docs/work/tip-durability-collapse.md):
         * floor on the GENUINE coins frontier. Wave 2: that frontier is
         * DERIVED — coins_applied_height-1 straight from progress.kv's own
         * co-committed state (no hash->block_map->height roundtrip through
         * the node_state cache). Legacy fallback: height(coins_best_block
         * HASH). Under-rewind is SAFE (more forward re-finalize, never
         * ahead); no rows deleted. */
        int64_t coins_best = -1;
        struct boot_derived_coins_best clamp_dcb;
        if (boot_derive_coins_best(&clamp_dcb)) {
            coins_best = clamp_dcb.height;
        } else {
            struct uint256 cbh; uint256_set_null(&cbh);
            if (g_coins_sqlite.db) coins_view_sqlite_get_best_block(&g_coins_sqlite, &cbh);
            struct block_index *cb = uint256_is_null(&cbh) ? NULL : block_map_find(&g_state.map_block_index, &cbh);
            coins_best = cb ? (int64_t)cb->nHeight : -1;
        }
        if (coins_best >= 0) {
            struct sqlite3 *pdb = progress_store_db();
            struct stage_reconcile_result rr;
            if (pdb &&
                stage_reconcile_clamp_tip_finalize_to_floor(
                    pdb, (int)coins_best, &rr) && rr.clamped) {
                printf("[boot] reducer reconcile: clamped tip_finalize cursor "
                       "to served-tip floor=%d (own height of the applied "
                       "coins frontier / deepest finalized row) — "
                       "re-finalizing forward\n",
                       rr.floor);
                service_state_transition_and_persist(SERVICE_STATE_RECONCILE,
                                      "reducer cursor/coins desync reconcile");
            }
        }
    }

    {
        int restored_h = active_chain_height(&g_state.chain_active);
        if (boot_restored_authority_tip && restored_h > 1000) {
            printf("[boot] skipping initial reducer activation after "
                   "authority restore h=%d; coordinator will advance after "
                   "RPC/P2P start\n",
                   restored_h);
            event_emitf(EV_BOOT_ACTIVATE, 0,
                        "skip_initial_activate restored_authority_tip=%d",
                        restored_h);
        } else {
            struct boot_phase bp_act;
            boot_phase_begin(&bp_act, "reducer_activation");
            struct activation_exec_outcome outcome;
            activation_request_connect(&g_activation_ctl, ACTIVATION_SRC_BOOT,
                                       NULL, &outcome);
            if (outcome.result == ACTIVATION_EXEC_FAILED)
                fprintf(stderr, "Warning: Failed to activate best chain: %s\n",
                        outcome.reason);
            boot_phase_end(&bp_act);
        }
    }

    /* final sweep. Post-activation is the last point at
     * which block_map and active_chain could still carry the anchor-
     * restore limp (nBits==0 entries, chain_active holes below tip); the
     * finalize also covers the block-file-scan + `Post-activation: fixed
     * N pprev heights from anchor` path in msg_headers that runs before
     * reducer activation completes. No-op when the integrity check
     * passes; logs a loud stderr line + fills what it can when it does
     * not. Safe on every boot — O(block_map) + one disk read per nBits
     * gap; the real live-node shape is ≤200 reads.
     *
     * Part L: honor the return value. A failed post-restore check at
     * a non-trivial tip height means the on-disk state is corrupted
     * past automatic repair (e.g. 3M holes in active_chain). The user
     * framing — "be brutal, fail fast" — means we refuse to proceed
     * into a half-loaded state. Operators who want the legacy "log
     * loud, continue" behavior must opt in with -allow-degraded. */
    /* Rewind a placeholder tip.
     *
     * If a placeholder (nBits==0) ever became our active tip — e.g.,
     * because a chain_restore anchor at h=N was promoted into the
     * active chain before its real block data arrived — every
     * subsequent header will be rejected with bad-diffbits since
     * GetNextWorkRequired sees prev_bits=0. Walk pprev until we hit
     * a real block, set THAT as the active tip, and let gap-fill
     * resync from there. */
    {
        struct block_index *tip = active_chain_tip(&g_state.chain_active);
        if (tip && tip->nBits == 0 && tip->nHeight > 0) {
            struct block_index *walk = tip->pprev;
            int last_h = tip->nHeight + 1;
            int steps = 0;
            while (walk && walk->nBits == 0 &&
                   walk->pprev && walk->pprev->nHeight < walk->nHeight &&
                   walk->nHeight < last_h && steps++ < 10000) {
                last_h = walk->nHeight;
                walk = walk->pprev;
            }
            if (walk && walk->nBits != 0) {
                fprintf(stderr,
                    "[boot] placeholder tip at h=%d (nBits=0); rewinding "
                    "to last real block h=%d\n",
                    tip->nHeight, walk->nHeight);
                if (boot_promote_tip_via_csr(
                        walk, "rewind_placeholder_tip", true) &&
                    walk->phashBlock) {
                    if (g_node_db.open)
                        (void)coins_rewind_above_tip(
                            g_node_db.db, walk->nHeight, -1);
                }
            } else {
                fprintf(stderr,
                    "[boot] placeholder tip at h=%d but no valid "
                    "ancestor found within 10000 steps; chain stuck\n",
                    tip->nHeight);
            }
        }
    }

    {
        struct boot_phase bp_fin;
        boot_phase_begin(&bp_fin, "chain_restore_finalize");
        service_state_advance(SERVICE_STATE_RECONCILE,
                              "post-restore finalize gate");
        struct zcl_result finalize_r =
            chain_restore_finalize(&g_state, ctx->datadir);
        bool finalize_ok = finalize_r.ok;
        if (!finalize_ok)
            fprintf(stderr,
                    "[boot] chain_restore_finalize failed: code=%d msg=%s\n",
                    finalize_r.code, finalize_r.message);
        boot_phase_end(&bp_fin);
        int  tip_h = active_chain_height(&g_state.chain_active);

        /* Classify the post-restore integrity result on its structured
         * breakdown — the finalize bool discards it. A RECONCILABLE
         * divergence (active_chain window holes, with no zero-nbits and
         * no height/pprev mismatch) is normal coins-application lag:
         * headers/bodies are ahead of the applied tip, NOT corruption.
         * It is NEVER fatal — the node enters DEGRADED_SERVING and the
         * condition engine reconciles it forward, so the process always
         * boots into an observable state instead of crash-looping. Only
         * true structural corruption (zero nbits in the tip window, or
         * active_chain height/pprev mismatches) stays fatal, and then
         * LOUD + observable, never a silent loop.
         *
         * This does NOT weaken any consensus gate: the integrity check
         * only counts NULL active_chain[] slots. The consensus gates
         * (connect_block prevhash, CSR rejection, find_most_work_chain
         * validity/HAVE_DATA filters, PoW/sig/proof verification) are
         * untouched and still reject bad blocks. */
        struct chain_integrity_result integ;
        chain_integrity_check_post_restore(&integ, &g_state);
        enum chain_integrity_class cls = chain_integrity_classify(&integ);

        if (!finalize_ok && tip_h > 1000) {
            if (cls == CHAIN_INTEGRITY_UNRECOVERABLE && !ctx->allow_degraded) {
                /* Crash-only: the reindex-recoverable shape (zero_nbits==0, a
                 * derived tip above the validated index extent) auto-requests a
                 * self-rebuild; structural corruption / exhausted budget pages.
                 * Verb check first: replay-from-blocks/ needs genesis-side
                 * block data, which a cold-import datadir does not have —
                 * probe h=1 readability so the classifier never exits into an
                 * impossible rebuild (defect #6). */
                bool reindex_ok = false;
                {
                    struct block_index *probe =
                        active_chain_at(&g_state.chain_active, 1);
                    struct block pblk;
                    if (probe && read_block_from_disk_index(&pblk, probe,
                                                            ctx->datadir)) {
                        block_free(&pblk);
                        reindex_ok = true;
                    }
                }
                if (boot_crashonly_handle_unrecoverable(ctx->datadir, tip_h,
                        integ.zero_nbits_count, integ.active_chain_mismatches,
                        integ.first_mismatch_height, reindex_ok))
                    return false;
                /* Reindex unexecutable (cold-import window): the page fired,
                 * no sentinel was written — serve degraded; the reducer
                 * reconciles forward exactly like the RECONCILABLE shape. */
                service_state_transition_and_persist(
                    SERVICE_STATE_DEGRADED_SERVING,
                    "reindex unexecutable on cold-import window");
            } else if (cls == CHAIN_INTEGRITY_UNRECOVERABLE) {
                fprintf(stderr,
                    "[boot] WARNING: post-restore structural corruption at "
                    "tip_h=%d; serving DEGRADED because -allow-degraded was "
                    "set.\n", tip_h);
                event_emitf(EV_BOOT_ACTIVATE, 0,
                    "degraded_serving allow_degraded_corruption tip=%d",
                    tip_h);
                service_state_transition_and_persist(SERVICE_STATE_DEGRADED_SERVING,
                    "allow-degraded over structural corruption");
            } else {
                /* RECONCILABLE: coins-application lag. Self-heal; never
                 * exit. The node serves at the contiguous applied tip
                 * while the condition engine reconciles forward. */
                fprintf(stderr,
                    "[boot] post-restore integrity: reconcilable divergence "
                    "at tip_h=%d (tip_window_holes=%d first_hole_h=%d) — "
                    "entering DEGRADED_SERVING; condition engine reconciles "
                    "forward. Not fatal.\n",
                    tip_h, integ.tip_window_holes,
                    integ.first_tip_window_hole);
                event_emitf(EV_BOOT_ACTIVATE, 0,
                    "degraded_serving reconcilable_integrity tip=%d holes=%d",
                    tip_h, integ.tip_window_holes);
                service_state_advance(SERVICE_STATE_DEGRADED_SERVING,
                    "reconcilable post-restore divergence");
            }
            (void)service_state_persist_to_progress_store();
        } else {
            /* Clean integrity — the rebuild (if any) converged; clear the budget. */
            boot_crashonly_clear(ctx->datadir);
            service_state_transition_and_persist(SERVICE_STATE_SYNCING,
                                  "post-restore integrity clean");
        }
    }

    /* Auto-scan wallet for transactions in connected blocks.
     * This ensures balance shows immediately after LDB import or
     * snapshot sync — no manual replaywalletfromchain needed.
     * A power node should just work. */
    {
        int tip_h = active_chain_height(&g_state.chain_active);
        if (tip_h > 0 && g_node_db.open) {
            struct boot_phase bp_ws;
            boot_phase_begin(&bp_ws, "wallet_scan_blocks");
            int found = wallet_scan_blocks(&g_node_db,
                &g_state.chain_active, &g_wallet, ctx->datadir,
                0, tip_h);
            boot_phase_end(&bp_ws);
            if (found > 0)
                printf("Wallet: auto-discovered %d transactions "
                       "(blocks 0-%d)\n", found, tip_h);
        }
    }

    /* Timing only (no behavior change): mark the start of the
     * finalize-and-build span — finalize_chain_state, shielded backfill,
     * and svc-ctx build — which sat uninstrumented between the
     * wallet_scan_blocks boot_phase and the p2p_services_start marker. */
    int64_t t_finalize_build = boot_clock_ms();

    boot_step_finalize_chain_state();
    struct block_index *tip = active_chain_tip(&g_state.chain_active);

    boot_step_backfill_shielded_if_needed(ctx, tip);

    /* Skip services if no_services flag is set (speedrun / benchmarking) */
    if (ctx->no_services) {
        printf("Boot complete (no_services mode). "
               "Chain tip: height=%d\n",
               active_chain_height(&g_state.chain_active));
        return true;
    }

    /* Runtime services: mempool, P2P, RPC, Tor, wallet sync (boot_services.c) */
    struct boot_svc_ctx svc;
    boot_step_build_svc_ctx(ctx, &svc);
    /* g_svc is stored so app_shutdown can access it */
    g_svc = svc;

    /* Emit crash recovery complete if boot succeeded */
    {
        int chain_h = active_chain_height(&g_state.chain_active);
        event_emitf(EV_CRASH_RECOVERY_COMPLETE, 0,
            "chain_height=%d", chain_h);
    }

    printf("[boot] %-30s %lldms\n", "finalize_and_build",
           (long long)(boot_clock_ms() - t_finalize_build));

    t_phase = boot_clock_ms();
    bool svc_ok = app_init_services(ctx, params, &g_svc);
    printf("[boot] %-30s %lldms\n", "p2p_services_start",
           (long long)(boot_clock_ms() - t_phase));
    printf("[boot] %-30s %lldms\n", "total",
           (long long)(boot_clock_ms() - t_boot_start));
    if (svc_ok)
        boot_stage_advance_to(BOOT_STAGE_READY);
    return svc_ok;
}

/* AS-safe SIGALRM backstop: app_shutdown_svc arms alarm(90), but shutdown
 * paths that never ran main.c's signal_handler() (chain-tip watchdog,
 * `stop` RPC, self-heal hot-loop) would otherwise die via SIGALRM's
 * DEFAULT disposition mid-teardown — silent, and unlogged. */
static void shutdown_alarm_abort(int sig)
{
    (void)sig;
    static const char msg[] =
        "[shutdown] alarm fired: graceful shutdown hung - forcing exit\n";
    (void)!write(STDERR_FILENO, msg, sizeof(msg) - 1);
    _exit(1);
}

/* Written LAST in app_shutdown: marker present == every teardown step
 * completed. An alarm-forced exit mid-shutdown must read as UNCLEAN so
 * the next boot runs recovery (it used to be written first). */
static void write_clean_shutdown_marker(void)
{
    if (!g_datadir) return;
    char path[1024];
    snprintf(path, sizeof(path), "%s/.shutdown_clean", g_datadir);
    FILE *f = fopen(path, "w");
    if (f) {
        fprintf(f, "%ld\n", (long)platform_time_wall_time_t());
        fclose(f);
    }
}

/* app_shutdown delegates to boot_services.c */
void app_shutdown(void)
{
    boot_stage_advance_to(BOOT_STAGE_SHUTDOWN_REQUESTED);
    /* Backstop must be live BEFORE app_shutdown_svc arms alarm(90). */
    signal(SIGALRM, shutdown_alarm_abort);
    boot_stop_platform_services();
    app_shutdown_svc(&g_svc);
    boot_step_stop_postmortem();
    write_clean_shutdown_marker();
    release_datadir_lock();
    boot_stage_advance_to(BOOT_STAGE_SHUTDOWN_COMPLETE);
}
bool app_is_running(void) { return atomic_load(&g_running); }

/* app_add_node, app_start_metrics, app_stop_metrics: boot_services.c */
