diff --git a/Build-Scripts/build-common.sh b/Build-Scripts/build-common.sh new file mode 100755 index 0000000000..c3b7988a3e --- /dev/null +++ b/Build-Scripts/build-common.sh @@ -0,0 +1,271 @@ +# Shared helpers for out-of-tree, per-platform Verium builds. +# Each platform uses its own: +# build// configure + object files +# depends// cached dependency toolchain +# out-/ binaries +# release-/ stripped binaries (where applicable) +# +# Never configure at the repo root — that pollutes src/ and breaks cross-compiles. + +build_common_root() { + cd "$(dirname "${BASH_SOURCE[0]}")/.." + BUILD_COMMON_ROOT="$(pwd)" +} + +# Remove accidental in-tree configure/build artifacts (not platform build dirs). +clean_root_configure_artifacts() { + local root="${1:-$BUILD_COMMON_ROOT}" + cd "$root" + + if [ -f config.status ] || [ -f Makefile ]; then + echo "=== Cleaning stale in-tree configure at repo root ===" + rm -f config.status config.log Makefile libtool + rm -rf src/config/bitcoin-config.h src/config/stamp-h1 + find src -name '*.o' -delete + find src -name '*.a' -delete + find src -name '*.lo' -delete + find src -name '.deps' -type d -prune -exec rm -rf {} + 2>/dev/null || true + fi + + # Subproject configure leakage from old in-tree builds + rm -rf src/univalue/config.* src/univalue/Makefile src/univalue/libtool src/univalue/.libs + rm -rf src/secp256k1/config.* src/secp256k1/Makefile src/secp256k1/libtool src/secp256k1/.libs + rm -f src/secp256k1/src/libsecp256k1-config.h +} + +ensure_autogen() { + local root="${1:-$BUILD_COMMON_ROOT}" + cd "$root" + if [ ! -x configure ]; then + ./autogen.sh + fi +} + +# Source shared/depends-preseed helpers (env + depends-cache.sh). +source_shared_preseed_helpers() { + local root="${1:-$BUILD_COMMON_ROOT}" + local cache_root="" + + if [ -n "${SHARED_DEPENDS_PRESEED:-}" ] && [ -f "${SHARED_DEPENDS_PRESEED}/depends-cache.sh" ]; then + cache_root="${SHARED_DEPENDS_PRESEED}" + elif [ -d "${root}/../shared/depends-preseed" ]; then + cache_root="$(cd "${root}/../shared/depends-preseed" && pwd)" + fi + + if [ -z "$cache_root" ] || [ ! -f "${cache_root}/depends-cache.sh" ]; then + echo "ERROR: shared depends preseed not found at ${root}/../shared/depends-preseed" >&2 + echo " Populate it with shared/depends-preseed/preseed-depends.sh" >&2 + return 1 + fi + + if [ -f "${cache_root}/env.sh" ]; then + # shellcheck source=/dev/null + source "${cache_root}/env.sh" + fi + export DEPENDS_PRESEED_ROOT="${SHARED_DEPENDS_PRESEED:-$cache_root}" + export SHARED_DEPENDS_PRESEED="${SHARED_DEPENDS_PRESEED:-$cache_root}" + # shellcheck source=/dev/null + source "${cache_root}/depends-cache.sh" +} + +ensure_depends() { + local host_triplet="$1" + local root="${2:-$BUILD_COMMON_ROOT}" + local extra_make_args="${3:-}" + cd "$root" + + source_shared_preseed_helpers "$root" || exit 1 + ensure_depends_with_shared_preseed "$host_triplet" "$root" "$extra_make_args" 4 +} + +# Docker: always mount CodeRepo/shared/depends-preseed at /shared/depends-preseed. +docker_shared_preseed_mount_args() { + local root="${1:-$BUILD_COMMON_ROOT}" + local shared + source_shared_preseed_helpers "$root" || exit 1 + shared="$(default_shared_preseed_for_project "$root")" || exit 1 + printf '%s\n' "-v" "${shared}:${DOCKER_SHARED_DEPENDS_PRESEED:-/shared/depends-preseed}" \ + "-e" "SHARED_DEPENDS_PRESEED=${DOCKER_SHARED_DEPENDS_PRESEED:-/shared/depends-preseed}" +} + +configure_platform_build() { + local build_dir="$1" + local host_triplet="$2" + local configure_extra="${3:-}" + local root="${4:-$BUILD_COMMON_ROOT}" + cd "$root" + mkdir -p "$build_dir" + cd "$build_dir" + + export CONFIG_SITE="${root}/depends/${host_triplet}/share/config.site" + local dep="${root}/depends/${host_triplet}" + + if [ -f config.status ]; then + echo "=== Reusing existing ${build_dir} (incremental) ===" + return 0 + fi + + echo "=== Configuring ${build_dir} for ${host_triplet} ===" + rm -f config.cache + # shellcheck disable=SC2086 + ../../configure --host="$host_triplet" --prefix="$dep" --with-gui=qt5 \ + --with-qt-bindir="$dep/native/bin" --with-qt-incdir="$dep/include" --with-qt-libdir="$dep/lib" \ + --disable-bench --disable-tests --enable-reduce-exports \ + $configure_extra +} + +ensure_secp256k1_gen_context() { + local build_dir="$1" + local root="${2:-$BUILD_COMMON_ROOT}" + cd "${root}/${build_dir}/src/secp256k1" + if [ -x gen_context ]; then + return 0 + fi + echo "=== Building native gen_context for ${build_dir} ===" + gcc-9 -I../../../../src/secp256k1/src -I../../../../src/secp256k1 \ + -c ../../../../src/secp256k1/src/gen_context.c -o gen_context.o + gcc-9 gen_context.o -o gen_context +} + +platform_make() { + local build_dir="$1" + local root="${2:-$BUILD_COMMON_ROOT}" + local jobs="${3:-4}" + cd "${root}/${build_dir}" + make -j"$jobs" -C src/univalue + make -j"$jobs" +} + +prepare_output_dirs() { + local root="${1:-$BUILD_COMMON_ROOT}" + local out_dir="$2" + local release_dir="${3:-}" + mkdir -p "${root}/${out_dir}" + if [ -n "$release_dir" ]; then + mkdir -p "${root}/${release_dir}" + fi +} + +# --- Windows Developer Edition (debug machine) -------------------------------- + +WINDOWS_DEV_HOST_TRIPLET="${WINDOWS_DEV_HOST_TRIPLET:-x86_64-w64-mingw32}" +WINDOWS_DEV_BUILD_DIR="${WINDOWS_DEV_BUILD_DIR:-build/windows-dev}" +WINDOWS_DEV_OUT_DIR="${WINDOWS_DEV_OUT_DIR:-out-windows-dev}" +WINDOWS_DEV_RELEASE_DIR="${WINDOWS_DEV_RELEASE_DIR:-release-windows-dev}" + +require_dev_helper_enabled() { + local root="${1:-$BUILD_COMMON_ROOT}" + if ! grep -q '^#define ENABLE_DEV_HELPER_WINDOW 1' "${root}/src/util/devhelperconfig.h" 2>/dev/null; then + echo "ERROR: Developer Edition requires ENABLE_DEV_HELPER_WINDOW 1 in src/util/devhelperconfig.h" >&2 + echo " Set it to 1 on this debug machine, then re-run." >&2 + echo " For holder/release builds use Build-Scripts/build-windows-docker.sh with the flag at 0." >&2 + exit 1 + fi +} + +ensure_windows_cross_toolchain() { + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y build-essential automake libtool pkg-config python3 \ + g++-mingw-w64-x86-64 binutils-mingw-w64-x86-64 \ + curl zip unzip gcc-9 g++-9 nsis git + + update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100 + update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-9 100 + update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix + update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix + + export RC="${WINDOWS_DEV_HOST_TRIPLET}-windres" + export WINDRES="${WINDOWS_DEV_HOST_TRIPLET}-windres" +} + +patch_curl_mk_for_windows_cross() { + local root="${1:-$BUILD_COMMON_ROOT}" + local f="${root}/depends/packages/curl.mk" + if [ -f "$f" ] && ! grep -q 'CI: cross-compile opts' "$f"; then + cat >> "$f" <<'EOF' + +# CI: cross-compile opts for Windows +$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp --disable-smb +$(package)_config_opts_mingw32 += --with-winssl +$(package)_config_opts_mingw64 += --with-winssl +$(package)_conf_env += ac_cv_func_strerror_r=no ac_cv_strerror_r_char_p=no ac_cv_func_clock_gettime=no ac_cv_header_dlfcn_h=no ac_cv_have_decl_strerror_r=yes +EOF + fi +} + +patch_time_cpp_for_windows_cross() { + local root="${1:-$BUILD_COMMON_ROOT}" + if [ -f "${root}/src/util/time.cpp" ] && ! grep -q gmtime_r_compat "${root}/src/util/time.cpp"; then + { + echo '#ifdef _WIN32' + echo '#include ' + echo 'static inline struct tm* gmtime_r_compat(const time_t* t, struct tm* res){ return gmtime_s(res,t)==0 ? res : NULL; }' + echo '#define gmtime_r(t,r) gmtime_r_compat((t),(r))' + echo '#endif' + cat "${root}/src/util/time.cpp" + } > "${root}/src/util/time.cpp.tmp" && mv "${root}/src/util/time.cpp.tmp" "${root}/src/util/time.cpp" + fi +} + +copy_windows_dev_binaries() { + local root="${1:-$BUILD_COMMON_ROOT}" + local build_dir="${2:-$WINDOWS_DEV_BUILD_DIR}" + local out_dir="${3:-$WINDOWS_DEV_OUT_DIR}" + local release_dir="${4:-$WINDOWS_DEV_RELEASE_DIR}" + + prepare_output_dirs "$root" "$out_dir" "$release_dir" + cp -f "${root}/${build_dir}/src/"*.exe "${root}/${out_dir}/" 2>/dev/null || true + cp -f "${root}/${build_dir}/src/qt/"*.exe "${root}/${out_dir}/" 2>/dev/null || true + cp -f "${root}/${build_dir}/release/"*.exe "${root}/${release_dir}/" 2>/dev/null || true +} + +clean_windows_dev_output_dir() { + local root="${1:-$BUILD_COMMON_ROOT}" + local out_dir="${2:-$WINDOWS_DEV_OUT_DIR}" + if [ -d "${root}/${out_dir}" ]; then + echo "=== Cleaning ${out_dir}/ before dev build ===" + rm -rf "${root}/${out_dir}" + fi +} + +clean_windows_dev_build_dir() { + local root="${1:-$BUILD_COMMON_ROOT}" + local build_dir="${2:-$WINDOWS_DEV_BUILD_DIR}" + if [ -d "${root}/${build_dir}" ]; then + echo "=== Cleaning ${build_dir}/ before dev build ===" + rm -rf "${root}/${build_dir}" + fi +} + +# Compile all Windows dev binaries and build the Developer Edition NSIS installer. +run_windows_dev_compile_and_package() { + local root="${1:-$BUILD_COMMON_ROOT}" + local host_triplet="${WINDOWS_DEV_HOST_TRIPLET}" + local build_dir="${WINDOWS_DEV_BUILD_DIR}" + local out_dir="${WINDOWS_DEV_OUT_DIR}" + + require_dev_helper_enabled "$root" + clean_windows_dev_build_dir "$root" "$build_dir" + clean_windows_dev_output_dir "$root" "$out_dir" + ensure_windows_cross_toolchain + patch_curl_mk_for_windows_cross "$root" + patch_time_cpp_for_windows_cross "$root" + + clean_root_configure_artifacts "$root" + ensure_depends "$host_triplet" "$root" "RC=\$RC WINDRES=\$WINDRES" + ensure_autogen "$root" + configure_platform_build "$build_dir" "$host_triplet" \ + '--disable-shared --enable-static ac_cv_search_clock_gettime=no' "$root" + ensure_secp256k1_gen_context "$build_dir" "$root" + platform_make "$build_dir" "$root" 4 + + copy_windows_dev_binaries "$root" "$build_dir" "$out_dir" "$WINDOWS_DEV_RELEASE_DIR" + + chmod +x "${root}/Build-Scripts/package-windows-dev-installer.sh" + "${root}/Build-Scripts/package-windows-dev-installer.sh" "$root" + + echo "=== Windows Developer Edition recompile complete ===" + echo "Binaries + installer: ${root}/${out_dir}/" + ls -la "${root}/${out_dir}/" +} diff --git a/Build-Scripts/build-in-docker.sh b/Build-Scripts/build-in-docker.sh index 1c285e2e2e..ab1cc6928c 100755 --- a/Build-Scripts/build-in-docker.sh +++ b/Build-Scripts/build-in-docker.sh @@ -1,79 +1,51 @@ #!/bin/bash -# Run full Linux64 build inside ubuntu-22.04 container (matches GitHub Actions) +# Linux x64 native build (out-of-tree). +# build/linux64/ + depends/x86_64-pc-linux-gnu/ + out-linux64/ +# +# Depends always sync from CodeRepo/shared/depends-preseed (see build-common.sh). set -e cd "$(dirname "$0")/.." +ROOT="$(pwd)" + +# shellcheck source=build-common.sh +source Build-Scripts/build-common.sh +mapfile -t PRESEED_MOUNT < <(docker_shared_preseed_mount_args "$ROOT") + +PLATFORM=linux64 HOST_TRIPLET=x86_64-pc-linux-gnu +BUILD_DIR="build/${PLATFORM}" +OUT_DIR="out-${PLATFORM}" docker run --rm \ - -v "$(pwd):/build" \ + -v "$ROOT:/build" \ + "${PRESEED_MOUNT[@]}" \ -w /build \ ubuntu:22.04 \ bash -c " set -e + source Build-Scripts/build-common.sh + build_common_root + export DEBIAN_FRONTEND=noninteractive apt-get update -qq - apt-get install -y gcc-9 g++-9 gcc-11 g++-11 build-essential automake libtool pkg-config python3 curl zip unzip ccache - update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100 \ - --slave /usr/bin/g++ g++ /usr/bin/g++-9 \ - --slave /usr/bin/gcov gcov /usr/bin/gcov-9 \ - --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-9 \ - --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-9 \ - --slave /usr/bin/gcc-nm gcc-nm /usr/bin/gcc-nm-9 - export CC=gcc-9 CXX=g++-9 HOST_TRIPLET=$HOST_TRIPLET - gcc --version - g++ --version - - echo '=== Priming FreeType with GCC 11 ===' - make -C depends HOST=\$HOST_TRIPLET CC=gcc-11 CXX=g++-11 freetype -j\$(nproc) - - echo '=== Building full depends ===' - make -C depends HOST=\$HOST_TRIPLET CC=gcc-9 CXX=g++-9 -j\$(nproc) + apt-get install -y build-essential automake libtool pkg-config python3 \ + curl git bison ca-certificates gcc-9 g++-9 - echo '=== Autogen ===' - ./autogen.sh + update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100 + update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-9 100 - echo '=== Configuring ===' - export CONFIG_SITE=\"\$(pwd)/depends/\$HOST_TRIPLET/share/config.site\" - DEP=\"\$(pwd)/depends/\$HOST_TRIPLET\" - export BOOST_CPPFLAGS=\"-I\$DEP/include\" - export BOOST_LDFLAGS=\"-L\$DEP/lib\" - export CPPFLAGS=\"\$BOOST_CPPFLAGS\" - export LDFLAGS=\"\$BOOST_LDFLAGS\" - bs=\"\$(ls \"\$DEP/lib\"/libboost_system*.a 2>/dev/null | head -n1 || true)\" - if [ -z \"\$bs\" ]; then echo 'ERROR: No libboost_system'; exit 1; fi - suf=\"\${bs##*/}\"; suf=\"\${suf#libboost_system}\"; suf=\"\${suf%.a}\" - export BOOST_LIB_SUFFIX=\"\$suf\" - export BOOST_THREAD_LIB_SUFFIX=\"\$suf\" - export LIBS=\"-pthread -lrt\" - CC=gcc-9 CXX=g++-9 ./configure --host=\$HOST_TRIPLET --prefix=\"\$DEP\" --with-gui=qt5 \\ - --with-qt-bindir=\"\$DEP/native/bin\" --with-qt-incdir=\"\$DEP/include\" --with-qt-libdir=\"\$DEP/lib\" \\ - --with-boost=\"\$DEP\" --with-boost-libdir=\"\$DEP/lib\" \\ - --disable-bench --disable-tests --enable-reduce-exports --disable-shared --enable-static + clean_root_configure_artifacts /build + ensure_depends ${HOST_TRIPLET} /build + ensure_autogen /build + configure_platform_build ${BUILD_DIR} ${HOST_TRIPLET} '' /build + platform_make ${BUILD_DIR} /build 4 - echo '=== Building ===' - make clean - make CC=\$CC CXX=\$CXX -j\$(nproc) + cd /build + prepare_output_dirs /build ${OUT_DIR} + cp -f ${BUILD_DIR}/src/veriumd ${BUILD_DIR}/src/verium-cli ${BUILD_DIR}/src/verium-tx \ + ${BUILD_DIR}/src/verium-wallet ${BUILD_DIR}/src/qt/verium-qt /build/${OUT_DIR}/ + chown 1000:1000 /build/${OUT_DIR}/* 2>/dev/null || true - echo '=== Packaging ===' - V=\$(grep '^PACKAGE_VERSION' Makefile 2>/dev/null | sed 's/.*= *//' | tr -d ' ') || echo '1.3.5.2' - OUTDIR=\"out-linux\" - rm -rf \"\$OUTDIR\" - mkdir -p \"\$OUTDIR\"/{daemon,doc,manpages,share/applications,share/pixmaps,share/icons/hicolor/128x128/apps} - cp -f src/qt/verium-qt \"\$OUTDIR/\" - cp -f COPYING \"\$OUTDIR/\" - cp -f doc/README_windows.txt \"\$OUTDIR/readme.txt\" - cp -f src/veriumd src/verium-cli src/verium-tx src/verium-wallet \"\$OUTDIR/daemon/\" 2>/dev/null || true - cp -r doc \"\$OUTDIR/\" - rm -rf \"\$OUTDIR/doc/man\" - find \"\$OUTDIR/doc\" -name 'Makefile*' -delete 2>/dev/null || true - ./contrib/release-tools/gather-manpages.sh \"\$OUTDIR/manpages\" - cp -f share/applications/verium-qt.desktop \"\$OUTDIR/share/applications/\" - cp -f share/pixmaps/verium-qt.png \"\$OUTDIR/share/pixmaps/\" - cp -f share/pixmaps/verium-qt.png \"\$OUTDIR/share/icons/hicolor/128x128/apps/\" - cp -f contrib/release-tools/INSTALL_LINUX.txt \"\$OUTDIR/\" - PKG=\"verium-\${V}-x86_64-pc-linux-gnu.tar.gz\" - tar -C \"\$OUTDIR\" -czf \"\$PKG\" . - sha256sum \"\$PKG\" > \"\${PKG}.SHA256SUMS\" - echo \"Built: \$PKG\" - ls -la \"\$PKG\" \"\${PKG}.SHA256SUMS\" + echo '=== Linux x64 build complete ===' + ls -la /build/${OUT_DIR}/ " diff --git a/Build-Scripts/build-windows-dev-docker.sh b/Build-Scripts/build-windows-dev-docker.sh new file mode 100755 index 0000000000..8116a6c38a --- /dev/null +++ b/Build-Scripts/build-windows-dev-docker.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Canonical Windows Developer Edition recompile (debug machine). +# +# Prerequisites: ENABLE_DEV_HELPER_WINDOW 1 in src/util/devhelperconfig.h +# +# Produces: +# out-windows-dev/verium-qt.exe (+ cli, daemon, wallet, tx) +# out-windows-dev/Verium--DeveloperEdition-win64-setup-unsigned.exe +# +# Install dir on Windows: Program Files\Verium Developer Edition +# (separate from release — will not overwrite holder installs) +# +# Aliases: recompile-dev-windows.sh, build-windows-dev-installer-docker.sh +set -e +cd "$(dirname "$0")/.." +ROOT="$(pwd)" + +# shellcheck source=build-common.sh +source Build-Scripts/build-common.sh +require_dev_helper_enabled "$ROOT" + +mapfile -t PRESEED_MOUNT < <(docker_shared_preseed_mount_args "$ROOT") + +docker run --rm \ + -v "$ROOT:/build" \ + "${PRESEED_MOUNT[@]}" \ + -w /build \ + ubuntu:22.04 \ + bash -c " + set -e + source Build-Scripts/build-common.sh + build_common_root + run_windows_dev_compile_and_package /build + " diff --git a/Build-Scripts/build-windows-dev-installer-docker.sh b/Build-Scripts/build-windows-dev-installer-docker.sh new file mode 100755 index 0000000000..b85a4be411 --- /dev/null +++ b/Build-Scripts/build-windows-dev-installer-docker.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Alias for the canonical dev recompile script. +exec "$(dirname "$0")/build-windows-dev-docker.sh" "$@" diff --git a/Build-Scripts/build-windows-docker.sh b/Build-Scripts/build-windows-docker.sh index ea42c2be2d..99001657f7 100755 --- a/Build-Scripts/build-windows-docker.sh +++ b/Build-Scripts/build-windows-docker.sh @@ -1,92 +1,64 @@ #!/bin/bash -# Run Windows x64 cross-compile build inside ubuntu-22.04 (matches Windows64Build workflow) +# Windows x64 cross-compile (out-of-tree) — holder / release builds. +# build/windows/ + depends/x86_64-w64-mingw32/ + out-windows/ +# +# Depends always sync from CodeRepo/shared/depends-preseed (see build-common.sh). +# For Developer Edition use Build-Scripts/build-windows-dev-docker.sh → out-windows-dev/ set -e cd "$(dirname "$0")/.." +ROOT="$(pwd)" + +# shellcheck source=build-common.sh +source Build-Scripts/build-common.sh +mapfile -t PRESEED_MOUNT < <(docker_shared_preseed_mount_args "$ROOT") + +PLATFORM=windows HOST_TRIPLET=x86_64-w64-mingw32 +BUILD_DIR="build/${PLATFORM}" +OUT_DIR="out-${PLATFORM}" +RELEASE_DIR="release-${PLATFORM}" docker run --rm \ - -v "$(pwd):/build" \ + -v "$ROOT:/build" \ + "${PRESEED_MOUNT[@]}" \ -w /build \ ubuntu:22.04 \ bash -c " set -e + source Build-Scripts/build-common.sh + build_common_root + export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y build-essential automake libtool pkg-config python3 \ g++-mingw-w64-x86-64 binutils-mingw-w64-x86-64 \ - curl zip unzip ccache gcc-9 g++-9 nsis + curl zip unzip gcc-9 g++-9 nsis git + update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100 update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-9 100 update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix export RC=\${HOST_TRIPLET}-windres WINDRES=\${HOST_TRIPLET}-windres - export HOST_TRIPLET=$HOST_TRIPLET - - echo '=== Hotfix sync.h / logging.h ===' - [ -f src/sync.h ] && ! grep -q '' src/sync.h && awk '{print; if (\$0~/#include[[:space:]]*/) {print \"#include \"; print \"#include \"}}' src/sync.h > src/sync.h.new && mv src/sync.h.new src/sync.h || true - [ -f src/logging.h ] && ! grep -q '' src/logging.h && awk '{print; if (\$0~/#include[[:space:]]*/) {print \"#include \"}}' src/logging.h > src/logging.h.new && mv src/logging.h.new src/logging.h || true - - echo '=== Patch curl.mk for Win64 ===' - f=depends/packages/curl.mk - [ -f \"\$f\" ] && { - echo '' >> \"\$f\" - echo '# CI: cross-compile opts for Windows' >> \"\$f\" - echo '\$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp' >> \"\$f\" - echo '\$(package)_config_opts_mingw32 += --with-winssl' >> \"\$f\" - echo '\$(package)_config_opts_mingw64 += --with-winssl' >> \"\$f\" - echo '\$(package)_conf_env += ac_cv_func_strerror_r=no ac_cv_strerror_r_char_p=no ac_cv_func_clock_gettime=no ac_cv_header_dlfcn_h=no ac_cv_have_decl_strerror_r=yes' >> \"\$f\" - rm -rf depends/work/build/*/curl/ - } - - echo '=== Build depends (cap -j4 for OOM) ===' - make -C depends HOST=\$HOST_TRIPLET CC_FOR_BUILD=gcc-9 CXX_FOR_BUILD=g++-9 -j4 RC=\"\$RC\" WINDRES=\"\$WINDRES\" || { - find depends/work/build -name config.log -exec sh -c 'echo \"==> {}\"; tail -n 80 \"{}\"' \\; - exit 1 - } - - echo '=== Patch chainparams seeds ===' - [ -f src/chainparams.cpp ] && perl -0777 -pe 's/vSeeds\\.emplace_back\\(\\s*\"[^\"]+\"\\s*,\\s*\"([^\"]+)\"\\s*(?:,\\s*(?:true|false))?\\s*\\)/vSeeds.push_back(\"\$1\")/g' -i src/chainparams.cpp || true - [ -f src/chainparams.cpp ] && perl -0777 -pe 's/vSeeds\\.push_back\\(\\s*CDNSSeedData\\(\\s*\"[^\"]+\"\\s*,\\s*\"([^\"]+)\"\\s*(?:,\\s*(?:true|false))?\\s*\\)\\s*\\)/vSeeds.push_back(\"\$1\")/g' -i src/chainparams.cpp || true - - echo '=== gmtime_r shim for Windows ===' - if [ -f src/util/time.cpp ] && ! grep -q 'gmtime_r_compat' src/util/time.cpp; then - { echo '#ifdef _WIN32'; echo '#include '; echo 'static inline struct tm* gmtime_r_compat(const time_t* t, struct tm* res){ return gmtime_s(res,t)==0 ? res : NULL; }'; echo '#define gmtime_r(t,r) gmtime_r_compat((t),(r))'; echo '#endif'; cat src/util/time.cpp; } > src/util/time.cpp.tmp && mv src/util/time.cpp.tmp src/util/time.cpp - fi - - echo '=== Init Qt automake vars ===' - [ -f src/Makefile.qt.include ] && ! grep -q '^[[:space:]]*LIBBITCOINQT_LIBS[[:space:]]*=' src/Makefile.qt.include && sed -i '1i LIBBITCOINQT_LIBS =\nLIBBITCOINQT_INCLUDES =' src/Makefile.qt.include || true - echo '=== Autogen + Configure ===' - ./autogen.sh - export CONFIG_SITE=\"\$(pwd)/depends/\$HOST_TRIPLET/share/config.site\" - rm -f config.cache - DEP=\"\$(pwd)/depends/\$HOST_TRIPLET\" - ./configure --host=\$HOST_TRIPLET --prefix=\"\$DEP\" --with-gui=qt5 \\ - --with-qt-bindir=\"\$DEP/native/bin\" --with-qt-incdir=\"\$DEP/include\" --with-qt-libdir=\"\$DEP/lib\" \\ - --disable-bench --disable-tests --enable-reduce-exports --disable-shared --enable-static \\ - ac_cv_search_clock_gettime=no + patch_curl_mk_for_windows_cross /build + patch_time_cpp_for_windows_cross /build + clean_root_configure_artifacts /build + ensure_depends ${HOST_TRIPLET} /build \"RC=\\\$RC WINDRES=\\\$WINDRES\" + ensure_autogen /build + configure_platform_build ${BUILD_DIR} ${HOST_TRIPLET} '--disable-shared --enable-static ac_cv_search_clock_gettime=no' /build + ensure_secp256k1_gen_context ${BUILD_DIR} /build + platform_make ${BUILD_DIR} /build 4 - echo '=== Build (cap -j4 to reduce OOM risk) ===' - make clean - make -j4 + cd /build/${BUILD_DIR} + make deploy || true - echo '=== Package ===' - V=\$(git describe --tags --dirty --always 2>/dev/null || echo untagged) - PKG=\"verium-\${V}-\${HOST_TRIPLET}.zip\" - mkdir -p out - cp -f src/*.exe out/ 2>/dev/null || true - cp -f src/qt/*.exe out/ 2>/dev/null || true - [ -n \"\$(ls -A out)\" ] || { echo 'No Windows EXEs'; exit 2; } - ./contrib/release-tools/gather-manpages.sh out/share/man/man1 - (cd out && zip -r \"../\$PKG\" .) - sha256sum \"\$PKG\" > \"\${PKG}.SHA256SUMS\" + prepare_output_dirs /build ${OUT_DIR} ${RELEASE_DIR} + cp -f src/*.exe /build/${OUT_DIR}/ 2>/dev/null || true + cp -f src/qt/*.exe /build/${OUT_DIR}/ 2>/dev/null || true + cp -f release/*.exe /build/${RELEASE_DIR}/ 2>/dev/null || true + cp -f *win64-setup*.exe /build/${OUT_DIR}/ 2>/dev/null || true - echo '=== NSIS Installer ===' - make deploy - SETUP=\$(ls verium-*-win64-setup.exe 2>/dev/null | head -1) - [ -n \"\$SETUP\" ] || { echo 'NSIS installer not built'; exit 3; } - sha256sum \"\$SETUP\" >> \"\${PKG}.SHA256SUMS\" - echo \"Built: \$PKG, \$SETUP\" - ls -la \"\$PKG\" \"\$SETUP\" \"\${PKG}.SHA256SUMS\" + echo '=== Windows build complete ===' + ls -la /build/${OUT_DIR}/ /build/${RELEASE_DIR}/ 2>/dev/null || true " diff --git a/Build-Scripts/package-windows-dev-installer.sh b/Build-Scripts/package-windows-dev-installer.sh new file mode 100755 index 0000000000..d8d640cc3d --- /dev/null +++ b/Build-Scripts/package-windows-dev-installer.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# Package a Windows NSIS installer for the Developer Edition build. +# +# Uses unstripped binaries from out-windows-dev/ (better for local debugging). +# Default install dir: Program Files\Verium Developer Edition +# +# Usage: +# ./Build-Scripts/package-windows-dev-installer.sh +# ./Build-Scripts/package-windows-dev-installer.sh /path/to/repo +# +# Requires: makensis, dev binaries in out-windows-dev/, ENABLE_DEV_HELPER_WINDOW=1 +set -euo pipefail + +ROOT="${1:-$(cd "$(dirname "$0")/.." && pwd)}" +BINARY_DIR="${BINARY_DIR:-${ROOT}/out-windows-dev}" +STAGING_DIR="${STAGING_DIR:-${ROOT}/installer-windows-dev/staging}" +OUT_DIR="${OUT_DIR:-${ROOT}/out-windows-dev}" +NSI_TEMPLATE="${ROOT}/share/setup-dev.nsi.in" +NSI_GENERATED="${ROOT}/installer-windows-dev/setup-dev.nsi" + +if ! grep -q '^#define ENABLE_DEV_HELPER_WINDOW 1' "${ROOT}/src/util/devhelperconfig.h" 2>/dev/null; then + echo "ERROR: Developer Edition packaging requires ENABLE_DEV_HELPER_WINDOW 1 in src/util/devhelperconfig.h" >&2 + exit 1 +fi + +read_version_from_configure() { + local cfg="${ROOT}/configure.ac" + local major minor revision build + major=$(sed -n 's/^define(_CLIENT_VERSION_MAJOR, //p' "$cfg" | tr -d ' )') + minor=$(sed -n 's/^define(_CLIENT_VERSION_MINOR, //p' "$cfg" | tr -d ' )') + revision=$(sed -n 's/^define(_CLIENT_VERSION_REVISION, //p' "$cfg" | tr -d ' )') + build=$(sed -n 's/^define(_CLIENT_VERSION_BUILD, //p' "$cfg" | tr -d ' )') + if [ -z "$major" ] || [ -z "$minor" ] || [ -z "$revision" ]; then + echo "ERROR: could not read version from ${cfg}" >&2 + exit 1 + fi + if [ -n "$build" ] && [ "$build" != "0" ]; then + echo "${major}.${minor}.${revision}.${build}" + else + echo "${major}.${minor}.${revision}" + fi +} + +VERSION="$(read_version_from_configure)" +VERSION_QUAD="$(echo "$VERSION" | awk -F. '{printf "%s.%s.%s.%s", $1, $2, $3, ($4==""?0:$4)}')" +COPYRIGHT_YEAR="$(date +%Y)" +INSTALLER_NAME="Verium-${VERSION}-DeveloperEdition-win64-setup-unsigned.exe" +INSTALLER_PATH="${OUT_DIR}/${INSTALLER_NAME}" + +REQUIRED_BINARIES=( + verium-qt.exe + veriumd.exe + verium-cli.exe + verium-tx.exe + verium-wallet.exe +) + +for bin in "${REQUIRED_BINARIES[@]}"; do + if [ ! -f "${BINARY_DIR}/${bin}" ]; then + echo "ERROR: missing ${BINARY_DIR}/${bin}" >&2 + echo " Run Build-Scripts/build-windows-dev-docker.sh (or recompile-dev-windows.sh) first." >&2 + exit 1 + fi +done + +if ! command -v makensis >/dev/null 2>&1; then + echo "ERROR: makensis not found (install NSIS)" >&2 + exit 1 +fi + +mkdir -p "$STAGING_DIR" "$OUT_DIR" "$(dirname "$NSI_GENERATED")" + +cat > "${STAGING_DIR}/DEV_EDITION_windows.txt" < "$NSI_GENERATED" + +echo "=== Building Developer Edition Windows installer ===" +echo "Binaries: ${BINARY_DIR}" +echo "Output: ${INSTALLER_PATH}" +makensis -V2 "$NSI_GENERATED" + +ls -lh "$INSTALLER_PATH" +echo "=== Developer Edition installer ready ===" diff --git a/Build-Scripts/preseed-depends.sh b/Build-Scripts/preseed-depends.sh new file mode 100755 index 0000000000..af3d86268d --- /dev/null +++ b/Build-Scripts/preseed-depends.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Preseed depends for Verium 2.2 — delegates to shared monorepo preseed. +exec "$(cd "$(dirname "$0")/../.." && pwd)/shared/depends-preseed/preseed-depends.sh" \ + "$(cd "$(dirname "$0")/.." && pwd)" "$@" diff --git a/Build-Scripts/recompile-dev-windows.sh b/Build-Scripts/recompile-dev-windows.sh new file mode 100755 index 0000000000..b85a4be411 --- /dev/null +++ b/Build-Scripts/recompile-dev-windows.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Alias for the canonical dev recompile script. +exec "$(dirname "$0")/build-windows-dev-docker.sh" "$@" diff --git a/configure.ac b/configure.ac index 5952519995..6bf308fc5c 100644 --- a/configure.ac +++ b/configure.ac @@ -3,7 +3,7 @@ AC_PREREQ([2.60]) define(_CLIENT_VERSION_MAJOR, 2) define(_CLIENT_VERSION_MINOR, 2) define(_CLIENT_VERSION_REVISION, 0) -define(_CLIENT_VERSION_BUILD, 0) +define(_CLIENT_VERSION_BUILD, 1) define(_CLIENT_VERSION_RC, 0) define(_CLIENT_VERSION_IS_RELEASE, false) define(_COPYRIGHT_YEAR, 2026) diff --git a/depends/packages/curl.mk b/depends/packages/curl.mk index 084b97977d..db819ba952 100644 --- a/depends/packages/curl.mk +++ b/depends/packages/curl.mk @@ -17,6 +17,7 @@ $(package)_config_opts+=--enable-symbol-hiding $(package)_config_opts+=--without-librtmp $(package)_config_opts+=--disable-rtsp $(package)_config_opts+=--disable-alt-svc +$(package)_config_opts+=--disable-smb $(package)_config_opts+=--disable-shared --enable-static $(package)_config_opts +=--without-libpsl --without-libidn2 --without-nghttp2 --without-ngtcp2 $(package)_config_opts +=--without-brotli --without-zstd --without-gsasl @@ -45,55 +46,7 @@ define $(package)_stage_cmds endef # CI: cross-compile opts for Windows -$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp -$(package)_config_opts_mingw32 += --with-winssl -$(package)_config_opts_mingw64 += --with-winssl -$(package)_conf_env += ac_cv_func_strerror_r=no ac_cv_strerror_r_char_p=no ac_cv_func_clock_gettime=no ac_cv_header_dlfcn_h=no ac_cv_have_decl_strerror_r=yes - -# CI: cross-compile opts for Windows -$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp -$(package)_config_opts_mingw32 += --with-winssl -$(package)_config_opts_mingw64 += --with-winssl -$(package)_conf_env += ac_cv_func_strerror_r=no ac_cv_strerror_r_char_p=no ac_cv_func_clock_gettime=no ac_cv_header_dlfcn_h=no ac_cv_have_decl_strerror_r=yes - -# CI: cross-compile opts for Windows -$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp -$(package)_config_opts_mingw32 += --with-winssl -$(package)_config_opts_mingw64 += --with-winssl -$(package)_conf_env += ac_cv_func_strerror_r=no ac_cv_strerror_r_char_p=no ac_cv_func_clock_gettime=no ac_cv_header_dlfcn_h=no ac_cv_have_decl_strerror_r=yes - -# CI: cross-compile opts for Windows -$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp -$(package)_config_opts_mingw32 += --with-winssl -$(package)_config_opts_mingw64 += --with-winssl -$(package)_conf_env += ac_cv_func_strerror_r=no ac_cv_strerror_r_char_p=no ac_cv_func_clock_gettime=no ac_cv_header_dlfcn_h=no ac_cv_have_decl_strerror_r=yes - -# CI: cross-compile opts for Windows -$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp -$(package)_config_opts_mingw32 += --with-winssl -$(package)_config_opts_mingw64 += --with-winssl -$(package)_conf_env += ac_cv_func_strerror_r=no ac_cv_strerror_r_char_p=no ac_cv_func_clock_gettime=no ac_cv_header_dlfcn_h=no ac_cv_have_decl_strerror_r=yes - -# CI: cross-compile opts for Windows -$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp -$(package)_config_opts_mingw32 += --with-winssl -$(package)_config_opts_mingw64 += --with-winssl -$(package)_conf_env += ac_cv_func_strerror_r=no ac_cv_strerror_r_char_p=no ac_cv_func_clock_gettime=no ac_cv_header_dlfcn_h=no ac_cv_have_decl_strerror_r=yes - -# CI: cross-compile opts for Windows -$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp -$(package)_config_opts_mingw32 += --with-winssl -$(package)_config_opts_mingw64 += --with-winssl -$(package)_conf_env += ac_cv_func_strerror_r=no ac_cv_strerror_r_char_p=no ac_cv_func_clock_gettime=no ac_cv_header_dlfcn_h=no ac_cv_have_decl_strerror_r=yes - -# CI: cross-compile opts for Windows -$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp -$(package)_config_opts_mingw32 += --with-winssl -$(package)_config_opts_mingw64 += --with-winssl -$(package)_conf_env += ac_cv_func_strerror_r=no ac_cv_strerror_r_char_p=no ac_cv_func_clock_gettime=no ac_cv_header_dlfcn_h=no ac_cv_have_decl_strerror_r=yes - -# CI: cross-compile opts for Windows -$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp +$(package)_config_opts += --disable-debug --disable-curldebug --disable-ldap --disable-ldaps --without-libidn2 --without-libpsl --without-brotli --without-zstd --without-nghttp2 --without-ssh --without-libssh2 --without-rtmp --disable-smb $(package)_config_opts_mingw32 += --with-winssl $(package)_config_opts_mingw64 += --with-winssl $(package)_conf_env += ac_cv_func_strerror_r=no ac_cv_strerror_r_char_p=no ac_cv_func_clock_gettime=no ac_cv_header_dlfcn_h=no ac_cv_have_decl_strerror_r=yes diff --git a/share/setup-dev.nsi.in b/share/setup-dev.nsi.in new file mode 100644 index 0000000000..f2f2fb081a --- /dev/null +++ b/share/setup-dev.nsi.in @@ -0,0 +1,161 @@ +; Verium Developer Edition Windows installer (local debugging builds only). +; Generated by Build-Scripts/package-windows-dev-installer.sh — do not edit setup-dev.nsi directly. + +Name "Verium Developer Edition (64-bit)" + +RequestExecutionLevel highest +SetCompressor /SOLID lzma + +!define REGKEY "SOFTWARE\VeriumDevEdition" +!define COMPANY "Verium project (Developer Edition)" +!define URL https://vericonomy.com/ + +!define MUI_ICON "@ROOT@/share/pixmaps/verium.ico" +!define MUI_WELCOMEFINISHPAGE_BITMAP "@ROOT@/share/pixmaps/nsis-wizard.bmp" +!define MUI_HEADERIMAGE +!define MUI_HEADERIMAGE_RIGHT +!define MUI_HEADERIMAGE_BITMAP "@ROOT@/share/pixmaps/nsis-header.bmp" +!define MUI_FINISHPAGE_NOAUTOCLOSE +!define MUI_STARTMENUPAGE_REGISTRY_ROOT HKCU +!define MUI_STARTMENUPAGE_REGISTRY_KEY "SOFTWARE\VeriumDevEdition" +!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME StartMenuGroup +!define MUI_STARTMENUPAGE_DEFAULTFOLDER "Verium Developer Edition" +!define MUI_FINISHPAGE_RUN "$WINDIR\explorer.exe" +!define MUI_FINISHPAGE_RUN_PARAMETERS $INSTDIR\verium-qt.exe +!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico" +!define MUI_UNWELCOMEFINISHPAGE_BITMAP "@ROOT@/share/pixmaps/nsis-wizard.bmp" +!define MUI_UNFINISHPAGE_NOAUTOCLOSE + +!include Sections.nsh +!include MUI2.nsh +!include x64.nsh + +Var StartMenuGroup + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_STARTMENU Application $StartMenuGroup +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE English + +OutFile "@OUTFILE@" +InstallDir "$PROGRAMFILES64\Verium Developer Edition" +CRCCheck on +XPStyle on +BrandingText "Developer Edition — not for holder release" +ShowInstDetails show +VIProductVersion @VERSION_QUAD@ +VIAddVersionKey ProductName "Verium Developer Edition" +VIAddVersionKey ProductVersion "@VERSION@" +VIAddVersionKey CompanyName "${COMPANY}" +VIAddVersionKey CompanyWebsite "${URL}" +VIAddVersionKey FileVersion "@VERSION@" +VIAddVersionKey FileDescription "Developer Edition installer for Verium (local debugging)" +VIAddVersionKey LegalCopyright "Copyright (C) 2009-@COPYRIGHT_YEAR@ The Vericonomy Core developers" +InstallDirRegKey HKCU "${REGKEY}" Path +ShowUninstDetails show + +Section -Main SEC0000 + SetOutPath $INSTDIR + SetOverwrite on + File "@BINARY_DIR@/verium-qt.exe" + File /oname=COPYING.txt "@ROOT@/COPYING" + File /oname=readme.txt "@ROOT@/doc/README_windows.txt" + File /oname=DEV_EDITION.txt "@STAGING_DIR@/DEV_EDITION_windows.txt" + SetOutPath $INSTDIR\daemon + File "@BINARY_DIR@/veriumd.exe" + File "@BINARY_DIR@/verium-cli.exe" + File "@BINARY_DIR@/verium-tx.exe" + File "@BINARY_DIR@/verium-wallet.exe" + SetOutPath $INSTDIR\doc + File /r /x Makefile* "@ROOT@/doc\*.*" + SetOutPath $INSTDIR + WriteRegStr HKCU "${REGKEY}\Components" Main 1 +SectionEnd + +Section -post SEC0001 + WriteRegStr HKCU "${REGKEY}" Path $INSTDIR + SetOutPath $INSTDIR + WriteUninstaller $INSTDIR\uninstall.exe + !insertmacro MUI_STARTMENU_WRITE_BEGIN Application + CreateDirectory $SMPROGRAMS\$StartMenuGroup + CreateShortcut "$SMPROGRAMS\$StartMenuGroup\$(^Name).lnk" $INSTDIR\verium-qt.exe + CreateShortcut "$SMPROGRAMS\$StartMenuGroup\Uninstall $(^Name).lnk" $INSTDIR\uninstall.exe + CreateShortcut "$DESKTOP\$(^Name).lnk" $INSTDIR\verium-qt.exe + !insertmacro MUI_STARTMENU_WRITE_END + WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" DisplayName "$(^Name)" + WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" DisplayVersion "@VERSION@" + WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" Publisher "${COMPANY}" + WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" URLInfoAbout "${URL}" + WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" DisplayIcon $INSTDIR\verium-qt.exe + WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" UninstallString $INSTDIR\uninstall.exe + WriteRegDWORD HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" NoModify 1 + WriteRegDWORD HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" NoRepair 1 + WriteRegStr HKCR "Verium-Dev" "URL Protocol" "" + WriteRegStr HKCR "Verium-Dev" "" "URL:Verium Developer Edition" + WriteRegStr HKCR "Verium-Dev\DefaultIcon" "" $INSTDIR\verium-qt.exe + WriteRegStr HKCR "Verium-Dev\shell\open\command" "" '"$INSTDIR\verium-qt.exe" "%1"' +SectionEnd + +!macro SELECT_UNSECTION SECTION_NAME UNSECTION_ID + Push $R0 + ReadRegStr $R0 HKCU "${REGKEY}\Components" "${SECTION_NAME}" + StrCmp $R0 1 0 next${UNSECTION_ID} + !insertmacro SelectSection "${UNSECTION_ID}" + GoTo done${UNSECTION_ID} +next${UNSECTION_ID}: + !insertmacro UnselectSection "${UNSECTION_ID}" +done${UNSECTION_ID}: + Pop $R0 +!macroend + +Section /o -un.Main UNSEC0000 + Delete /REBOOTOK $INSTDIR\verium-qt.exe + Delete /REBOOTOK $INSTDIR\COPYING.txt + Delete /REBOOTOK $INSTDIR\readme.txt + Delete /REBOOTOK $INSTDIR\DEV_EDITION.txt + RMDir /r /REBOOTOK $INSTDIR\daemon + RMDir /r /REBOOTOK $INSTDIR\doc + DeleteRegValue HKCU "${REGKEY}\Components" Main +SectionEnd + +Section -un.post UNSEC0001 + DeleteRegKey HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" + Delete /REBOOTOK "$SMPROGRAMS\$StartMenuGroup\Uninstall $(^Name).lnk" + Delete /REBOOTOK "$SMPROGRAMS\$StartMenuGroup\$(^Name).lnk" + Delete /REBOOTOK $INSTDIR\uninstall.exe + Delete /REBOOTOK $INSTDIR\debug.log + Delete /REBOOTOK $INSTDIR\db.log + DeleteRegValue HKCU "${REGKEY}" StartMenuGroup + DeleteRegValue HKCU "${REGKEY}" Path + DeleteRegKey /IfEmpty HKCU "${REGKEY}\Components" + DeleteRegKey /IfEmpty HKCU "${REGKEY}" + DeleteRegKey HKCR "Verium-Dev" + RmDir /REBOOTOK $SMPROGRAMS\$StartMenuGroup + RmDir /REBOOTOK $INSTDIR + Push $R0 + StrCpy $R0 $StartMenuGroup 1 + StrCmp $R0 ">" no_smgroup +no_smgroup: + Pop $R0 +SectionEnd + +Function .onInit + InitPluginsDir + ${If} ${RunningX64} + SetRegView 64 + ${Else} + MessageBox MB_OK|MB_ICONSTOP "Cannot install 64-bit Developer Edition on a 32-bit system." + Abort + ${EndIf} +FunctionEnd + +Function un.onInit + ReadRegStr $INSTDIR HKCU "${REGKEY}" Path + !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuGroup + !insertmacro SELECT_UNSECTION Main ${UNSEC0000} +FunctionEnd diff --git a/src/Makefile.am b/src/Makefile.am index 7c16b7ece4..1d09b7619c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -513,6 +513,8 @@ libbitcoin_util_a_SOURCES = \ util/moneystr.cpp \ util/rbf.cpp \ util/threadnames.cpp \ + util/activitylog.cpp \ + util/devedition.cpp \ util/strencodings.cpp \ util/string.cpp \ util/time.cpp \ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 45616b3cd3..fae87251b2 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -46,6 +46,7 @@ QT_MOC_CPP = \ qt/moc_bitcoingui.cpp \ qt/moc_bitcoinunits.cpp \ qt/moc_bootstrapdialog.cpp \ + qt/moc_devhelperwindow.cpp \ qt/moc_clientmodel.cpp \ qt/moc_coincontroldialog.cpp \ qt/moc_coincontroltreewidget.cpp \ @@ -126,6 +127,8 @@ BITCOIN_QT_H = \ qt/bitcoingui.h \ qt/bitcoinunits.h \ qt/bootstrapdialog.h \ + qt/devhelperwindow.h \ + qt/devtools.h \ qt/clientmodel.h \ qt/coincontroldialog.h \ qt/coincontroltreewidget.h \ @@ -246,6 +249,8 @@ BITCOIN_QT_BASE_CPP = \ qt/bitcoingui.cpp \ qt/bitcoinunits.cpp \ qt/bootstrapdialog.cpp \ + qt/devhelperwindow.cpp \ + qt/devtools.cpp \ qt/clientmodel.cpp \ qt/csvmodelwriter.cpp \ qt/guiutil.cpp \ diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 29e70877fc..28ea9ec420 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -74,15 +74,19 @@ class CMainParams : public CChainParams { consensus.nPowTargetTimespan = 2 * 24 * 60 * 60; // two days consensus.fPowNoRetargeting = false; - // The best chain should have at least this much work. - consensus.nMinimumChainWork = uint256S("0x0000000000000000000000000000000000000000000000000000000000000000"); + // The best chain should have at least this much work (mainnet block 550000). + // Peers below this work are ignored; raises the bar against low-work forks. + consensus.nMinimumChainWork = uint256S("0x00000000000000000000000000000000000000000000000000000caa7ae09617"); - // By default assume that the signatures in ancestors of this block are valid. - consensus.defaultAssumeValid = uint256S("0x0000000000000000000000000000000000000000000000000000000000000000"); + // During IBD, skip script checks for blocks that are ancestors of this + // buried block (mainnet height 550000). PoW/merkle/connect rules still apply. + // Override with -assumevalid=0 to verify everything. Bump each release. + consensus.defaultAssumeValid = uint256S("0xcf444659c13aa06daae3cb6cbd697780a355e10b6a4d758ace78660bfd91ea61"); // Let's start with VIP (Verium Improvement Protocol) // XXX: Use it and set a correct value consensus.VIP1Height = 520000; // Change Min Fee + // Legacy 1.3.5.2 header/miner rules always on; stricter 2.x rules at this height. consensus.nTimeRulesActivationHeight = std::numeric_limits::max(); /** @@ -104,16 +108,9 @@ class CMainParams : public CChainParams { assert(consensus.hashGenesisBlock == uint256S("0x8232c0cf3bd7e05546e3d7aaaaf89fed8bc97c4df1a8c95e9249e13a2734932b")); assert(genesis.hashMerkleRoot == uint256S("0x925e430072a1f39b530fc79db162e29433ab0ea266a99c8cab4f03001dc9faa9")); - // Note that of those which support the service bits prefix, most only support a subset of - // possible options. - // This is fine at runtime as we'll fall back to using them as a oneshot if they don't support the - // service bits we want, but we should get them updated to support all service bits wanted by any - // release ASAP to avoid it where possible. - // vSeeds.emplace_back("seed.vrm.vericonomy.com"); - vSeeds.emplace_back("91.121.221.200"); - vSeeds.emplace_back("104.128.239.215"); - vSeeds.emplace_back("216.189.149.162"); - vSeeds.emplace_back("seeder.vrm.vericonomy.com"); + vFixedSeeds.clear(); + vSeeds.clear(); + vSeeds.emplace_back("seed.vrm.vericonomy.com"); base58Prefixes[PUBKEY_ADDRESS] = std::vector(1,70); base58Prefixes[SCRIPT_ADDRESS] = std::vector(1,132); @@ -164,8 +161,7 @@ class CTestNetParams : public CChainParams { // VIP (Verium Improvement Protocol) - same as mainnet for now consensus.VIP1Height = 520000; - // Testnet-only consensus activation point for stricter timestamp rules. - // Keep disabled while evaluating behavior with existing chaindata. + // Legacy 1.3.5.2 rules always on; set height when enabling 2.x hardfork on testnet. consensus.nTimeRulesActivationHeight = std::numeric_limits::max(); /** @@ -186,7 +182,7 @@ class CTestNetParams : public CChainParams { assert(consensus.hashGenesisBlock == uint256S("0x4cebbc55af4761b306f05630df20506cd351454eeb7e87b0c8eb5342cb3d7268")); assert(genesis.hashMerkleRoot == uint256S("0xb4e66f65015d59122e410e5253da361433cee45a50fed88c51f93e5165b5d157")); - // Testnet seeds (can be empty initially) + // Testnet: no DNS seeds yet — network is small; users bootstrap via -addnode vSeeds.clear(); // Testnet address prefixes (different from mainnet) diff --git a/src/clientversion.cpp b/src/clientversion.cpp index f2c83e3477..e021b5c90e 100644 --- a/src/clientversion.cpp +++ b/src/clientversion.cpp @@ -68,7 +68,7 @@ const std::string CLIENT_NAME("Verium"); const std::string CLIENT_BUILD(BUILD_DESC CLIENT_VERSION_SUFFIX); -static std::string FormatVersion(int nVersion) +std::string FormatVersion(int nVersion) { if (nVersion % 100 == 0) return strprintf("%d.%d.%d", nVersion / 1000000, (nVersion / 10000) % 100, (nVersion / 100) % 100); diff --git a/src/clientversion.h b/src/clientversion.h index 363094b696..f5164d93a2 100644 --- a/src/clientversion.h +++ b/src/clientversion.h @@ -47,6 +47,7 @@ extern const std::string CLIENT_BUILD; std::string FormatFullVersion(); std::string FormatSubVersion(const std::string& name, int nClientVersion, const std::vector& comments); +std::string FormatVersion(int nVersion); #endif // WINDRES_PREPROC diff --git a/src/consensus/params.h b/src/consensus/params.h index 9ccfaf5830..0d51f4f998 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -28,7 +28,7 @@ struct Params { /** VIP */ int VIP1Height; - /** Activation height for stricter timestamp consensus checks (INT_MAX disables). */ + /** Activation height for 2.x stricter timestamp rules (INT_MAX = legacy 1.3.5.2 only). */ int nTimeRulesActivationHeight; }; } // namespace Consensus diff --git a/src/downloader.cpp b/src/downloader.cpp index c3d4ddbb2a..dbe7ab7ff5 100644 --- a/src/downloader.cpp +++ b/src/downloader.cpp @@ -1,13 +1,28 @@ #include +#include #include #include +#include +#include #include +#include #include #define CURL_STATICLIB #include #include +#ifdef WIN32 +#include +#endif + +#include + +#include +#include +#include +#include +#include /* Downloader functions for bootstrapping and updating client software @@ -22,12 +37,149 @@ * XXX it could use some rate limiting */ static void* xferinfo_data = nullptr; +static BootstrapStatusFn g_bootstrap_status_fn = nullptr; +static std::atomic g_download_cancelled{false}; +static std::atomic g_bootstrap_retry_now{false}; +static std::once_flag g_curl_init_once; +static curl_off_t g_bootstrap_resume_offset = 0; +static std::string g_bootstrap_shutdown_hint; +static bool g_bootstrap_network_paused = false; + +extern std::unique_ptr g_connman; + +static const char* BOOTSTRAP_APPLY_PENDING_FILE = ".bootstrap_apply_pending"; + +fs::path GetBootstrapApplyPendingPath() +{ + return GetDataDir() / BOOTSTRAP_APPLY_PENDING_FILE; +} + +bool bootstrapStagingReady() +{ + const fs::path staging = GetDataDir() / "bootstrap"; + return fs::exists(staging / "blocks") && + fs::exists(staging / "chainstate"); +} + +bool bootstrapApplyPending() +{ + if (fs::exists(GetBootstrapApplyPendingPath())) + return true; + return bootstrapStagingReady(); +} + +void markBootstrapApplyPending() +{ + fsbridge::ofstream marker(GetBootstrapApplyPendingPath(), std::ios_base::app); + if (!marker.good()) + throw std::runtime_error("bootstrap: Unable to write bootstrap apply marker"); + marker.close(); + LogActivity("Bootstrap: staged chain data ready; will apply on next startup"); +} + +void clearBootstrapApplyPending() +{ + const fs::path marker = GetBootstrapApplyPendingPath(); + if (fs::exists(marker)) { + fs::remove(marker); + } +} + +void setBootstrapShutdownHint(const std::string& hint) +{ + g_bootstrap_shutdown_hint = hint; +} + +std::string getBootstrapShutdownHint() +{ + return g_bootstrap_shutdown_hint; +} + +void ensureDownloaderInit() +{ + std::call_once(g_curl_init_once, []() { + curl_global_init(CURL_GLOBAL_ALL); + }); +} + +static size_t curlWriteToFile(void* ptr, size_t size, size_t nmemb, void* userdata) +{ + return fwrite(ptr, size, nmemb, static_cast(userdata)); +} + +void set_download_cancelled(bool cancel) +{ + g_download_cancelled.store(cancel); +} + +bool download_cancelled() +{ + return g_download_cancelled.load(); +} + +void reset_download_cancel() +{ + g_download_cancelled.store(false); + g_bootstrap_retry_now.store(false); +} + +void requestBootstrapDownloadRetryNow() +{ + g_bootstrap_retry_now.store(true); +} + +bool pauseNetworkForBootstrap() +{ + SetChainSyncPausedForBootstrap(true); + + if (!g_connman) { + LogPrintf("bootstrap: connman not ready; chain sync paused only\n"); + return false; + } + if (g_bootstrap_network_paused) { + return true; + } + const bool prior = g_connman->GetNetworkActive(); + if (!prior) { + // SetNetworkActive(false) is a no-op when already inactive; toggle to force DisconnectNodes(). + g_connman->SetNetworkActive(true); + } + g_connman->SetNetworkActive(false); + g_bootstrap_network_paused = true; + LogActivity("Bootstrap: pausing P2P and chain sync during download and extract"); + LogPrintf("bootstrap: network paused for bootstrap (was %sactive)\n", prior ? "" : "in"); + return prior; +} + +void restoreNetworkAfterBootstrap() +{ + SetChainSyncPausedForBootstrap(false); + + if (!g_bootstrap_network_paused) { + return; + } + if (!g_connman) { + g_bootstrap_network_paused = false; + return; + } + g_connman->SetNetworkActive(true); + g_bootstrap_network_paused = false; + LogActivity("Bootstrap: resuming P2P and chain sync"); + LogPrintf("bootstrap: network restored (active=true)\n"); +} + static int xferinfo(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { + if (g_download_cancelled.load()) + return 1; void (*ptr)(curl_off_t, curl_off_t) = (void(*)(curl_off_t, curl_off_t))xferinfo_data; - if (ptr != nullptr) ptr(dltotal, dlnow); + if (ptr != nullptr) { + const curl_off_t now = g_bootstrap_resume_offset + dlnow; + const curl_off_t total = dltotal > 0 ? g_bootstrap_resume_offset + dltotal : dltotal; + ptr(now, total); + } return 0; // continue xfer. } @@ -36,15 +188,227 @@ void set_xferinfo_data(void* d) xferinfo_data = d; } +void set_bootstrap_status_fn(BootstrapStatusFn fn) +{ + g_bootstrap_status_fn = std::move(fn); +} + +static void bootstrap_status(const char* message) +{ + if (!message || !*message) + return; + LogActivity("%s", message); + if (g_bootstrap_status_fn) + g_bootstrap_status_fn(message); +} + +std::string getClientUrl() { + return CLIENT_URL_VRM; +} + +std::string getBootstrapArchiveFileName() { + return BOOTSTRAP_FILE_VRM; +} + +std::string getBootstrapDownloadUrl() { + return strprintf("%s%s/%s", getClientUrl(), BOOTSTRAP_DIR, getBootstrapArchiveFileName()); +} + +uint64_t getBootstrapPartialBytes() +{ + const fs::path pathBootstrapZip = GetDataDir() / getBootstrapArchiveFileName(); + if (!boost::filesystem::exists(pathBootstrapZip)) + return 0; + try { + return static_cast(boost::filesystem::file_size(pathBootstrapZip)); + } catch (...) { + return 0; + } +} + +void clearBootstrapPartial() +{ + const fs::path pathBootstrapZip = GetDataDir() / getBootstrapArchiveFileName(); + if (boost::filesystem::exists(pathBootstrapZip)) { + LogPrintf("bootstrap: Removing partial bootstrap archive %s\n", pathBootstrapZip.string()); + boost::filesystem::remove(pathBootstrapZip); + } +} + +static void waitBeforeBootstrapRetry(int seconds) +{ + g_bootstrap_retry_now.store(false); + for (int elapsed = 0; elapsed < seconds * 10; ++elapsed) { + if (g_download_cancelled.load()) { + throw std::runtime_error("Download cancelled."); + } + if (g_bootstrap_retry_now.load()) { + return; + } + UninterruptibleSleep(std::chrono::milliseconds{100}); + } +} + +static bool isBootstrapDownloadFatalError(const std::string& message) +{ + return message.find("Download: fatal:") != std::string::npos; +} + +/** Single curl attempt; may throw on transient errors (caller retries). */ +static void downloadBootstrapArchiveOnce(const std::string& url, const fs::path& target_file_path) +{ + LogPrintf("Download: Downloading bootstrap from %s.\n", url); + + ensureDownloaderInit(); + + curl_off_t resume_from = 0; + if (boost::filesystem::exists(target_file_path)) { + try { + resume_from = static_cast(boost::filesystem::file_size(target_file_path)); + if (resume_from > 0) { + LogPrintf("bootstrap: Resuming download at byte %lld\n", static_cast(resume_from)); + } + } catch (...) { + resume_from = 0; + } + } + + FILE* file = fsbridge::fopen(target_file_path, resume_from > 0 ? "ab" : "wb"); + if (!file) + throw std::runtime_error(strprintf("Download: error: Unable to open output file for writing: %s.", target_file_path.string().c_str())); + + CURL* curlHandle = curl_easy_init(); + if (!curlHandle) { + fclose(file); + throw std::runtime_error("Download: error: curl_easy_init failed."); + } + + char errbuf[CURL_ERROR_SIZE]; + errbuf[0] = 0; + + curl_easy_setopt(curlHandle, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(curlHandle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curlHandle, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curlHandle, CURLOPT_NOPROGRESS, 0); + curl_easy_setopt(curlHandle, CURLOPT_XFERINFODATA, xferinfo_data); + curl_easy_setopt(curlHandle, CURLOPT_XFERINFOFUNCTION, xferinfo); + curl_easy_setopt(curlHandle, CURLOPT_WRITEFUNCTION, curlWriteToFile); + curl_easy_setopt(curlHandle, CURLOPT_WRITEDATA, file); + curl_easy_setopt(curlHandle, CURLOPT_CONNECTTIMEOUT, 120L); + curl_easy_setopt(curlHandle, CURLOPT_TCP_KEEPALIVE, 1L); + /* Do not abort large bootstrap downloads for slow-but-steady links. */ + curl_easy_setopt(curlHandle, CURLOPT_LOW_SPEED_LIMIT, 0L); + curl_easy_setopt(curlHandle, CURLOPT_LOW_SPEED_TIME, 0L); + if (resume_from > 0) { + curl_easy_setopt(curlHandle, CURLOPT_RESUME_FROM_LARGE, resume_from); + } + + g_bootstrap_resume_offset = resume_from; + const CURLcode res = curl_easy_perform(curlHandle); + g_bootstrap_resume_offset = 0; + + if (g_download_cancelled.load()) { + curl_easy_cleanup(curlHandle); + fclose(file); + throw std::runtime_error("Download cancelled."); + } + + long response_code = 0; + curl_easy_getinfo(curlHandle, CURLINFO_RESPONSE_CODE, &response_code); + + if (res != CURLE_OK) { + curl_easy_cleanup(curlHandle); + fclose(file); + const uint64_t partial = getBootstrapPartialBytes(); + const std::string partial_msg = partial > 0 + ? strprintf(" Partial download saved (%llu MB). Retry will resume.", partial / (1024 * 1024)) + : std::string(); + size_t len = strlen(errbuf); + if (len) { + throw std::runtime_error(strprintf("Download: error: %s%s%s", errbuf, ((errbuf[len - 1] != '\n') ? "\n" : ""), partial_msg)); + } + throw std::runtime_error(strprintf("Download: error: %s.%s", curl_easy_strerror(res), partial_msg)); + } + + if (response_code == 416) { + curl_easy_cleanup(curlHandle); + fclose(file); + LogPrintf("bootstrap: Server rejected resume (416); removing partial and restarting download\n"); + boost::filesystem::remove(target_file_path); + throw std::runtime_error("Download: resume rejected by server."); + } + + if (response_code == 404 || response_code == 403 || response_code == 401) { + curl_easy_cleanup(curlHandle); + fclose(file); + throw std::runtime_error(strprintf("Download: fatal: Server responded with %ld.", response_code)); + } + + if (response_code != 200 && !(resume_from > 0 && response_code == 206)) { + curl_easy_cleanup(curlHandle); + fclose(file); + throw std::runtime_error(strprintf("Download: error: Server responded with %ld.", response_code)); + } + + curl_easy_cleanup(curlHandle); + fclose(file); + + LogPrintf("Download: Bootstrap archive download successful.\n"); +} + +/** Resume-capable download with automatic retry on flaky connections. */ +static void downloadBootstrapArchive(const std::string& url, const fs::path& target_file_path) +{ + int attempt = 0; + while (true) { + if (g_download_cancelled.load()) { + throw std::runtime_error("Download cancelled."); + } + ++attempt; + try { + downloadBootstrapArchiveOnce(url, target_file_path); + return; + } catch (const std::runtime_error& e) { + if (g_download_cancelled.load()) { + throw std::runtime_error("Download cancelled."); + } + const std::string msg = e.what(); + if (msg.find("cancelled") != std::string::npos) { + throw; + } + if (isBootstrapDownloadFatalError(msg)) { + throw; + } + + const uint64_t partial = getBootstrapPartialBytes(); + const int delay_sec = std::min(5 * attempt, 60); + const std::string status_msg = strprintf( + "Download interrupted at %llu MB (attempt %d). Retrying in %d seconds...", + partial / (1024 * 1024), attempt, delay_sec); + bootstrap_status(status_msg.c_str()); + LogPrintf("bootstrap: download attempt %d failed: %s\n", attempt, msg.c_str()); + + waitBeforeBootstrapRetry(delay_sec); + } + } +} + void downloadFile(std::string url, const fs::path& target_file_path) { LogPrintf("Download: Downloading from %s. \n", url); + ensureDownloaderInit(); + reset_download_cancel(); + FILE *file = fsbridge::fopen(target_file_path, "wb"); if( ! file ) throw std::runtime_error(strprintf("Download: error: Unable to open output file for writing: %s.", target_file_path.string().c_str())); CURL *curlHandle = curl_easy_init(); + if (!curlHandle) { + fclose(file); + throw std::runtime_error("Download: error: curl_easy_init failed."); + } CURLcode res; char errbuf[CURL_ERROR_SIZE]; @@ -57,11 +421,24 @@ void downloadFile(std::string url, const fs::path& target_file_path) { curl_easy_setopt(curlHandle, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curlHandle, CURLOPT_XFERINFODATA, xferinfo_data); curl_easy_setopt(curlHandle, CURLOPT_XFERINFOFUNCTION, xferinfo); + curl_easy_setopt(curlHandle, CURLOPT_WRITEFUNCTION, curlWriteToFile); curl_easy_setopt(curlHandle, CURLOPT_WRITEDATA, file); + /* Large bootstrap archives: allow slow links; fail only on prolonged stall. */ + curl_easy_setopt(curlHandle, CURLOPT_CONNECTTIMEOUT, 60L); + curl_easy_setopt(curlHandle, CURLOPT_LOW_SPEED_LIMIT, 512L); + curl_easy_setopt(curlHandle, CURLOPT_LOW_SPEED_TIME, 600L); res = curl_easy_perform(curlHandle); + if (g_download_cancelled.load()) { + curl_easy_cleanup(curlHandle); + fclose(file); + boost::filesystem::remove(target_file_path); + throw std::runtime_error("Download cancelled."); + } + if(res != CURLE_OK) { curl_easy_cleanup(curlHandle); + fclose(file); size_t len = strlen(errbuf); if(len) throw std::runtime_error(strprintf("Download: error: %s%s.", errbuf, ((errbuf[len - 1] != '\n') ? "\n" : ""))); @@ -71,8 +448,11 @@ void downloadFile(std::string url, const fs::path& target_file_path) { long response_code; curl_easy_getinfo(curlHandle, CURLINFO_RESPONSE_CODE, &response_code); - if( response_code != 200 ) + if( response_code != 200 ) { + curl_easy_cleanup(curlHandle); + fclose(file); throw std::runtime_error(strprintf("Download: error: Server responded with a %d .", response_code)); + } curl_easy_cleanup(curlHandle); fclose(file); @@ -83,44 +463,52 @@ void downloadFile(std::string url, const fs::path& target_file_path) { } +static unzFile OpenBootstrapZip(const fs::path& target_file_path) +{ + const fs::path abs_path = fs::absolute(target_file_path); +#ifdef WIN32 + /* Match fsbridge::fopen wide-path behavior; required for Zip64 (>4GB) archives on Windows. */ + zlib_filefunc64_def ffunc; + fill_win32_filefunc64W(&ffunc); + return unzOpen2_64(abs_path.wstring().c_str(), &ffunc); +#else + return unzOpen64(abs_path.string().c_str()); +#endif +} + // bootstrap +static void bootstrap_extract_progress(int files_done) +{ + const std::string msg = strprintf("Extracting archive (%d files processed)", files_done); + bootstrap_status(msg.c_str()); +} + void extractBootstrap(const fs::path& target_file_path) { LogPrintf("bootstrap: Extracting bootstrap %s.\n", target_file_path); if (!boost::filesystem::exists(target_file_path)) throw std::runtime_error("bootstrap: Bootstrap archive not found"); - - const char * zipfilename = target_file_path.string().c_str(); - unzFile uf; -#ifdef USEWIN32IOAPI - zlib_filefunc64_def ffunc; - fill_win32_filefunc64A(&ffunc); - uf = unzOpen2_64(zipfilename, &ffunc); -#else - uf = unzOpen64(zipfilename); -#endif + unzFile uf = OpenBootstrapZip(target_file_path); if (uf == NULL) - throw std::runtime_error(strprintf("bootstrap: Cannot open bootstrap archive: %s\n", zipfilename)); + throw std::runtime_error(strprintf("bootstrap: Cannot open bootstrap archive: %s\n", target_file_path.string())); const char * dest_subdir = nullptr; - if (!gArgs.GetBoolArg("-testnet", false)) { - /* Mainnet only: support zips with top-level blocks/chainstate (extract into bootstrap/ subdir) */ - char first_entry[256] = {0}; - if (zip_get_first_entry_name(uf, first_entry, sizeof(first_entry))) { - std::string name(first_entry); - if (name.find("bootstrap/") != 0) { - dest_subdir = "bootstrap"; - LogPrintf("bootstrap: Zip has top-level entries, extracting into bootstrap/.\n"); - } + char first_entry[256] = {0}; + if (zip_get_first_entry_name(uf, first_entry, sizeof(first_entry))) { + std::string name(first_entry); + if (name.find("bootstrap/") != 0) { + dest_subdir = "bootstrap"; + LogPrintf("bootstrap: Zip has top-level entries, extracting into bootstrap/.\n"); } } - /* Testnet: always use dest_subdir=nullptr (zip must have bootstrap/ prefix as before) */ - int unzip_err = zip_extract_all(uf, GetDataDir(), "bootstrap", dest_subdir); - if (unzip_err != UNZ_OK) + int unzip_err = zip_extract_all(uf, GetDataDir(), "bootstrap", dest_subdir, bootstrap_extract_progress); + if (unzip_err != UNZ_OK) { + unzClose(uf); throw std::runtime_error("bootstrap: Unzip failed\n"); + } unzClose(uf); LogPrintf("bootstrap: Unzip successful\n"); @@ -133,58 +521,174 @@ void validateBootstrapContent() { LogPrintf("bootstrap: Checking Bootstrap Content\n"); - if (!boost::filesystem::exists(GetDataDir() / "bootstrap" / "chainstate") || - !boost::filesystem::exists(GetDataDir() / "bootstrap" / "blocks")) - throw std::runtime_error("bootstrap: Downloaded zip file did not contain all necessary files!\n"); + const fs::path staging = GetDataDir() / "bootstrap"; + std::string missing; + if (!boost::filesystem::exists(staging / "blocks")) + missing += " blocks"; + if (!boost::filesystem::exists(staging / "chainstate")) + missing += " chainstate"; + if (!missing.empty()) { + throw std::runtime_error(strprintf( + "bootstrap: Downloaded zip file did not contain all necessary files (%s)!", + missing.substr(1))); + } + + if (!boost::filesystem::exists(staging / "indexes")) { + LogPrintf("bootstrap: archive has no indexes/; optional indexes will be rebuilt after install\n"); + LogActivity("Bootstrap: no indexes in archive (will rebuild on startup if enabled)"); + } } +static void copy_directory_recursive(const fs::path& src, const fs::path& dst) +{ + boost::system::error_code ec; + fs::create_directories(dst, ec); + if (ec) { + throw std::runtime_error(strprintf("bootstrap: Unable to create %s: %s", dst.string(), ec.message())); + } + + for (fs::directory_iterator it(src, ec), end; it != end; it.increment(ec)) { + if (ec) { + throw std::runtime_error(strprintf("bootstrap: Unable to read %s: %s", src.string(), ec.message())); + } + const fs::path from = it->path(); + const fs::path to = dst / from.filename(); + if (fs::is_directory(from)) { + copy_directory_recursive(from, to); + } else { + fs::create_directories(to.parent_path(), ec); + if (ec) { + throw std::runtime_error(strprintf("bootstrap: Unable to create %s: %s", to.parent_path().string(), ec.message())); + } + fs::copy_file(from, to, fs::copy_option::overwrite_if_exists, ec); + if (ec) { + throw std::runtime_error(strprintf("bootstrap: Unable to copy %s: %s", from.string(), ec.message())); + } + } + } +} + +static void install_staged_directory(const fs::path& src, const fs::path& dst) +{ + boost::system::error_code ec; + if (fs::exists(dst)) { + fs::remove_all(dst, ec); + if (ec) { + throw std::runtime_error(strprintf("bootstrap: Unable to remove %s: %s", dst.string(), ec.message())); + } + ec.clear(); + } + + fs::rename(src, dst, ec); + if (!ec) { + return; + } + + LogPrintf("bootstrap: rename %s -> %s failed (%s), copying instead\n", + src.string(), dst.string(), ec.message().c_str()); + copy_directory_recursive(src, dst); + fs::remove_all(src, ec); + if (ec) { + LogPrintf("bootstrap: warning: installed %s but failed to remove staging copy %s: %s\n", + dst.string(), src.string(), ec.message().c_str()); + } +} + void applyBootstrap() { + LogActivity("Bootstrap apply: removing old blocks directory"); boost::filesystem::remove_all(GetDataDir() / "blocks"); + LogActivity("Bootstrap apply: removing old chainstate directory"); boost::filesystem::remove_all(GetDataDir() / "chainstate"); - boost::filesystem::rename(GetDataDir() / "bootstrap" / "blocks", GetDataDir() / "blocks"); - boost::filesystem::rename(GetDataDir() / "bootstrap" / "chainstate", GetDataDir() / "chainstate"); + LogActivity("Bootstrap apply: removing old indexes directory"); + boost::filesystem::remove_all(GetDataDir() / "indexes"); + LogActivity("Bootstrap apply: installing blocks from staging"); + install_staged_directory(GetDataDir() / "bootstrap" / "blocks", GetDataDir() / "blocks"); + LogActivity("Bootstrap apply: installing chainstate from staging"); + install_staged_directory(GetDataDir() / "bootstrap" / "chainstate", GetDataDir() / "chainstate"); + if (boost::filesystem::exists(GetDataDir() / "bootstrap" / "indexes")) { + LogActivity("Bootstrap apply: installing indexes from staging"); + install_staged_directory(GetDataDir() / "bootstrap" / "indexes", GetDataDir() / "indexes"); + } else { + LogActivity("Bootstrap apply: no indexes in archive; will rebuild on startup if enabled"); + } + LogActivity("Bootstrap apply: removing staging directory"); boost::filesystem::remove_all(GetDataDir() / "bootstrap"); - boost::filesystem::path pathBootstrapTurbo(GetDataDir() / "bootstrap_VRM.zip"); + boost::filesystem::path pathBootstrapZip(GetDataDir() / getBootstrapArchiveFileName()); + boost::filesystem::path pathBootstrapLegacy(GetDataDir() / "bootstrap.zip"); boost::filesystem::path pathBootstrap(GetDataDir() / "bootstrap.dat"); - if (boost::filesystem::exists(pathBootstrapTurbo)){ - boost::filesystem::remove(pathBootstrapTurbo); + if (boost::filesystem::exists(pathBootstrapZip)){ + boost::filesystem::remove(pathBootstrapZip); + } + if (boost::filesystem::exists(pathBootstrapLegacy)){ + boost::filesystem::remove(pathBootstrapLegacy); } if (boost::filesystem::exists(pathBootstrap)){ boost::filesystem::remove(pathBootstrap); } + LogActivity("Bootstrap apply: complete"); } void downloadBootstrap() { - LogPrintf("bootstrap: Starting bootstrap process.\n"); + const std::string url = getBootstrapDownloadUrl(); + const std::string archiveName = getBootstrapArchiveFileName(); + + LogPrintf("bootstrap: Starting bootstrap from %s\n", url.c_str()); + + pauseNetworkForBootstrap(); - boost::filesystem::path pathBootstrapZip = GetDataDir() / "bootstrap_VRM.zip"; + ensureDownloaderInit(); + + boost::filesystem::path pathBootstrapZip = GetDataDir() / archiveName; boost::filesystem::path pathBootstrapStaging = GetDataDir() / "bootstrap"; - /* Remove any existing staging dir so redownload is a clean overwrite */ if (boost::filesystem::exists(pathBootstrapStaging)) { + bootstrap_status("Clearing previous bootstrap staging folder"); LogPrintf("bootstrap: Removing existing bootstrap staging directory for clean extract.\n"); boost::filesystem::remove_all(pathBootstrapStaging); } - downloadFile(BOOTSTRAP_URL, pathBootstrapZip); + const uint64_t partial = getBootstrapPartialBytes(); + if (partial > 0) { + const std::string resume_msg = strprintf("Resuming download of %s from %s (%llu MB saved)", + archiveName.c_str(), url.c_str(), partial / (1024 * 1024)); + bootstrap_status(resume_msg.c_str()); + } else { + const std::string start_msg = strprintf("Downloading %s from %s", archiveName.c_str(), url.c_str()); + bootstrap_status(start_msg.c_str()); + } + + downloadBootstrapArchive(url, pathBootstrapZip); + { + const std::string extract_msg = strprintf("Extracting %s (this can take several minutes)", archiveName.c_str()); + bootstrap_status(extract_msg.c_str()); + } extractBootstrap(pathBootstrapZip); + bootstrap_status("Validating blocks and chainstate"); validateBootstrapContent(); - fBootstrap = true; + markBootstrapApplyPending(); + setBootstrapShutdownHint("Bootstrap extracted. Restart Verium to install chain data and finish syncing."); + bootstrap_status("Bootstrap ready — restart Verium to install chain data"); LogPrintf("bootstrap: bootstrap process finished.\n"); - - return; } // check for update void downloadVersionFile() { LogPrintf("Check for update: Getting version file.\n"); - boost::filesystem::path pathVersionFile = GetDataDir() / "VERSION_VRM.json"; + boost::filesystem::path pathVersionFile = GetDataDir() / "VERSION.json"; - downloadFile(VERSIONFILE_URL, pathVersionFile); + try { + downloadFile(strprintf("%s/%d.%d/releases/%s%s", getClientUrl(), + CLIENT_VERSION_MAJOR, CLIENT_VERSION_MINOR, + FormatVersion(CLIENT_VERSION), + VERSIONFILE_PATH), pathVersionFile + ); + } catch (...) { + throw; + } return; } @@ -193,9 +697,12 @@ void downloadClient(std::string fileName) { LogPrintf("Check for update: Downloading new client.\n"); boost::filesystem::path pathClientFile = GetDataDir() / fileName; - std::string clientFileUrl = CLIENT_URL + fileName; - downloadFile(clientFileUrl, pathClientFile); + try { + downloadFile(strprintf("%s/%d.%d/releases/%s", getClientUrl(), CLIENT_VERSION_MAJOR, CLIENT_VERSION_MINOR, fileName), pathClientFile); + } catch (...) { + throw; + } return; } @@ -204,4 +711,4 @@ int getArchitecture() { int *i; return sizeof(i) * 8; // 8 bits/byte -} \ No newline at end of file +} diff --git a/src/downloader.h b/src/downloader.h index 7868a7c5c2..a4f4d512ef 100644 --- a/src/downloader.h +++ b/src/downloader.h @@ -1,21 +1,68 @@ #ifndef BITCOIN_DOWNLOADER_H #define BITCOIN_DOWNLOADER_H +#if defined(HAVE_CONFIG_H) +#include +#endif + #include +#include -#if defined(__arm__) || defined(__aarch64__) -const std::string BOOTSTRAP_URL("https://files.vericonomy.com/vrm/bootstrap-arm/bootstrap.zip"); -#else -const std::string BOOTSTRAP_URL("https://files.vericonomy.com/vrm/bootstrap/verium-bootstrap.zip"); -#endif +// Chain data is architecture-independent; all platforms (x86_64, ARM, ARM64) +// pull from the same hosted bootstrap directory. The old /bootstrap-arm path +// did not exist on the file server and produced a 404 on ARM builds. +const std::string BOOTSTRAP_DIR("/bootstrap"); + +const std::string BOOTSTRAP_FILE_VRM("verium-bootstrap.zip"); -const std::string VERSIONFILE_URL("https://files.vericonomy.com/vrm/VERSION_VRM.json"); -const std::string CLIENT_URL("https://files.vericonomy.com/vrm/"); +const std::string VERSIONFILE_PATH("/VERSION.json"); + +const std::string CLIENT_URL_VRM("https://files.vericonomy.com/vrm"); void downloadBootstrap(); void applyBootstrap(); +/** True if bootstrap chain data is staged and should be applied on next startup. */ +bool bootstrapApplyPending(); +/** Mark bootstrap as ready to apply (called after successful extract). */ +void markBootstrapApplyPending(); +void clearBootstrapApplyPending(); +/** Optional message for the shutdown window after bootstrap completes. */ +void setBootstrapShutdownHint(const std::string& hint); +std::string getBootstrapShutdownHint(); void downloadVersionFile(); void downloadClient(std::string fileName); int getArchitecture(); +/** Call once before any curl use (thread-safe). */ +void ensureDownloaderInit(); + +/** Hardcoded bootstrap archive name and full download URL (see downloader.h constants). */ +std::string getBootstrapArchiveFileName(); +std::string getBootstrapDownloadUrl(); + +/** Bytes already downloaded for the current bootstrap archive, if any. */ +uint64_t getBootstrapPartialBytes(); + +/** Remove a partial bootstrap archive (e.g. user chose to sync from network). */ +void clearBootstrapPartial(); + +/** Progress callback: void(curl_off_t now, curl_off_t total). Set via set_xferinfo_data. */ +void set_xferinfo_data(void* callback); + +/** Optional UI/status hook for bootstrap (may be called from worker thread). */ +using BootstrapStatusFn = std::function; +void set_bootstrap_status_fn(BootstrapStatusFn fn); + +void set_download_cancelled(bool cancel); +bool download_cancelled(); +void reset_download_cancel(); + +/** Skip the auto-retry wait and attempt download again immediately. */ +void requestBootstrapDownloadRetryNow(); + +/** Pause P2P (disconnect peers, stop new connections) while bootstrap runs. */ +bool pauseNetworkForBootstrap(); +/** Restore P2P when the user abandons bootstrap (Skip). No-op if not paused by us. */ +void restoreNetworkAfterBootstrap(); + #endif // BITCOIN_DOWNLOADER_H diff --git a/src/init.cpp b/src/init.cpp index e8fb665714..ebb5d4fc52 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -9,6 +9,8 @@ #include +#include + #include #include #include @@ -49,6 +51,8 @@ #include #include #include +#include +#include #include #include #include @@ -267,10 +271,24 @@ void Shutdown(InitInterfaces& interfaces) #endif if(fBootstrap) { + /* Legacy in-memory flag from older builds; current flow applies on next startup. */ try { + LogActivity("Bootstrap apply: legacy shutdown-path starting"); applyBootstrap(); + clearBootstrapApplyPending(); + fBootstrap = false; } catch(std::exception &e) { - LogPrintf("%s: Unable to change databse: %s\n",__func__,e.what()); + LogActivity("Bootstrap apply: legacy shutdown-path failed: %s", e.what()); + LogPrintf("%s: Unable to change database: %s\n",__func__,e.what()); + } + } else if (bootstrapApplyPending()) { + try { + LogActivity("Bootstrap apply: shutdown-path starting"); + applyBootstrap(); + clearBootstrapApplyPending(); + } catch (const std::exception& e) { + LogActivity("Bootstrap apply: shutdown-path failed: %s", e.what()); + LogPrintf("%s: Unable to apply bootstrap: %s\n", __func__, e.what()); } } @@ -846,6 +864,8 @@ void InitLogging() std::string version_string = FormatFullVersion(); #ifdef DEBUG version_string += " (debug build)"; +#elif !CLIENT_VERSION_IS_RELEASE + version_string += " (pre-release build)"; #else version_string += " (release build)"; #endif @@ -1201,6 +1221,35 @@ bool AppInitMain(InitInterfaces& interfaces) LogInstance().m_file_path.string())); } + InitActivityLog(); + InitDevHelperLogMirror(); + uiInterface.InitMessage_connect([](const std::string& message) { + LogActivityEx(ActivityLevel::Info, nullptr, 0, nullptr, "Init: %s", message.c_str()); + }); + uiInterface.ShowProgress_connect([](const std::string& title, int nProgress, bool resume_possible) { + (void)resume_possible; + LogActivityEx(ActivityLevel::Progress, nullptr, 0, nullptr, "Progress: %s (%d%%)", title.c_str(), nProgress); + }); + + LogActivity("Startup: AppInitMain entered"); +#if ENABLE_DEV_HELPER_WINDOW + if (IsDeveloperEditionActive()) { + LogActivityEx(ActivityLevel::Info, __FILE__, __LINE__, __func__, + "Developer Edition active (%s)", GetDeveloperEditionVersionString().c_str()); + } +#endif + + if (bootstrapApplyPending()) { + uiInterface.InitMessage(_("Applying bootstrap chain data...").translated); + try { + applyBootstrap(); + clearBootstrapApplyPending(); + } catch (const std::exception& e) { + LogActivity("Bootstrap apply: startup failed: %s", e.what()); + return InitError(strprintf(_("Failed to apply bootstrap: %s").translated, e.what())); + } + } + if (!LogInstance().m_log_timestamps) LogPrintf("Startup time: %s\n", FormatISO8601DateTime(GetTime())); LogPrintf("Default data directory %s\n", GetDefaultDataDir().string()); diff --git a/src/logging.cpp b/src/logging.cpp index 60ab486198..88f54f44b6 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -9,6 +9,10 @@ #include +namespace BCLog { +thread_local bool g_logger_in_print_callback{false}; +} + const char * const DEFAULT_DEBUGLOGFILE = "debug.log"; BCLog::Logger& LogInstance() @@ -67,6 +71,11 @@ bool BCLog::Logger::StartLogging() if (m_print_to_file) FileWriteStr(s, m_fileout); if (m_print_to_console) fwrite(s.data(), 1, s.size(), stdout); + for (const auto& cb : m_print_callbacks) { + BCLog::g_logger_in_print_callback = true; + cb(s); + BCLog::g_logger_in_print_callback = false; + } m_msgs_before_open.pop_front(); } @@ -81,6 +90,7 @@ void BCLog::Logger::DisconnectTestLogger() m_buffering = true; if (m_fileout != nullptr) fclose(m_fileout); m_fileout = nullptr; + m_print_callbacks.clear(); } void BCLog::Logger::EnableCategory(BCLog::LogFlags flag) @@ -270,6 +280,11 @@ void BCLog::Logger::LogPrintStr(const std::string& str) fwrite(str_prefixed.data(), 1, str_prefixed.size(), stdout); fflush(stdout); } + for (const auto& cb : m_print_callbacks) { + BCLog::g_logger_in_print_callback = true; + cb(str_prefixed); + BCLog::g_logger_in_print_callback = false; + } if (m_print_to_file) { assert(m_fileout != nullptr); diff --git a/src/logging.h b/src/logging.h index 75cd5353c0..a96c1bc029 100644 --- a/src/logging.h +++ b/src/logging.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,10 @@ struct CLogCategoryActive }; namespace BCLog { + +/** True while a print callback is running (avoids LogPrint re-entry deadlocks). */ +extern thread_local bool g_logger_in_print_callback; + enum LogFlags : uint32_t { NONE = 0, NET = (1 << 0), @@ -77,6 +82,9 @@ namespace BCLog { std::string LogTimestampStr(const std::string& str); + /** Slots that connect to the print signal */ + std::list> m_print_callbacks /* GUARDED_BY(m_cs) */ {}; + public: bool m_print_to_console = false; bool m_print_to_file = false; @@ -95,7 +103,22 @@ namespace BCLog { bool Enabled() const { std::lock_guard scoped_lock(m_cs); - return m_buffering || m_print_to_console || m_print_to_file; + return m_buffering || m_print_to_console || m_print_to_file || !m_print_callbacks.empty(); + } + + /** Connect a slot to the print signal and return the connection */ + std::list>::iterator PushBackCallback(std::function fun) + { + std::lock_guard scoped_lock(m_cs); + m_print_callbacks.push_back(std::move(fun)); + return --m_print_callbacks.end(); + } + + /** Delete a connection */ + void DeleteCallback(std::list>::iterator it) + { + std::lock_guard scoped_lock(m_cs); + m_print_callbacks.erase(it); } /** Start logging (and flush all buffered messages) */ diff --git a/src/logging/timer.h b/src/logging/timer.h new file mode 100644 index 0000000000..2b27c71080 --- /dev/null +++ b/src/logging/timer.h @@ -0,0 +1,104 @@ +// Copyright (c) 2009-2010 Satoshi Nakamoto +// Copyright (c) 2009-2019 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_LOGGING_TIMER_H +#define BITCOIN_LOGGING_TIMER_H + +#include +#include +#include + +#include +#include + + +namespace BCLog { + +//! RAII-style object that outputs timing information to logs. +template +class Timer +{ +public: + //! If log_category is left as the default, end_msg will log unconditionally + //! (instead of being filtered by category). + Timer( + std::string prefix, + std::string end_msg, + BCLog::LogFlags log_category = BCLog::LogFlags::ALL) : + m_prefix(std::move(prefix)), + m_title(std::move(end_msg)), + m_log_category(log_category) + { + this->Log(strprintf("%s started", m_title)); + m_start_t = GetTime(); + } + + ~Timer() + { + this->Log(strprintf("%s completed", m_title)); + } + + void Log(const std::string& msg) + { + const std::string full_msg = this->LogMsg(msg); + + if (m_log_category == BCLog::LogFlags::ALL) { + LogPrintf("%s\n", full_msg); + } else { + LogPrint(m_log_category, "%s\n", full_msg); + } + } + + std::string LogMsg(const std::string& msg) + { + const auto end_time = GetTime() - m_start_t; + if (m_start_t.count() <= 0) { + return strprintf("%s: %s", m_prefix, msg); + } + + std::string units = ""; + float divisor = 1; + + if (std::is_same::value) { + units = "μs"; + } else if (std::is_same::value) { + units = "ms"; + divisor = 1000.; + } else if (std::is_same::value) { + units = "s"; + divisor = 1000. * 1000.; + } + + const float time_ms = end_time.count() / divisor; + return strprintf("%s: %s (%.2f%s)", m_prefix, msg, time_ms, units); + } + +private: + std::chrono::microseconds m_start_t{}; + + //! Log prefix; usually the name of the function this was created in. + const std::string m_prefix{}; + + //! A descriptive message of what is being timed. + const std::string m_title{}; + + //! Forwarded on to LogPrint if specified - has the effect of only + //! outputting the timing log when a particular debug= category is specified. + const BCLog::LogFlags m_log_category{}; + +}; + +} // namespace BCLog + + +#define LOG_TIME_MICROS(end_msg, ...) \ + BCLog::Timer PASTE2(logging_timer, __COUNTER__)(__func__, end_msg, ## __VA_ARGS__) +#define LOG_TIME_MILLIS(end_msg, ...) \ + BCLog::Timer PASTE2(logging_timer, __COUNTER__)(__func__, end_msg, ## __VA_ARGS__) +#define LOG_TIME_SECONDS(end_msg, ...) \ + BCLog::Timer PASTE2(logging_timer, __COUNTER__)(__func__, end_msg, ## __VA_ARGS__) + + +#endif // BITCOIN_LOGGING_TIMER_H diff --git a/src/miner.cpp b/src/miner.cpp index c34866358a..a85bea0926 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -39,11 +39,31 @@ #include -int64_t UpdateTime(CBlockHeader* pblock, const Consensus::Params& consensusParams, const CBlockIndex* pindexPrev) +void SyncCoinbaseTimestamp(CBlock* pblock) +{ + if (pblock->vtx.empty() || !pblock->vtx[0]->IsCoinBase()) + return; + if (pblock->vtx[0]->nTime == pblock->nTime) + return; + + CMutableTransaction coinbaseTx(*pblock->vtx[0]); + coinbaseTx.nTime = pblock->nTime; + pblock->vtx[0] = MakeTransactionRef(std::move(coinbaseTx)); +} + +int64_t UpdateTime(CBlock* pblock, const Consensus::Params& consensusParams, const CBlockIndex* pindexPrev) { int64_t nOldTime = pblock->nTime; int64_t nNewTime = std::max(pindexPrev->GetMedianTimePast()+1, GetAdjustedTime()); + const int nHeight = pindexPrev->nHeight + 1; + if (nHeight >= consensusParams.nTimeRulesActivationHeight) { + nNewTime = std::max(nNewTime, pindexPrev->GetBlockTime() - MAX_FUTURE_BLOCK_TIME); + for (const auto& tx : pblock->vtx) { + nNewTime = std::max(nNewTime, tx->nTime); + } + } + if (nOldTime < nNewTime) pblock->nTime = nNewTime; @@ -159,6 +179,9 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc // Fill in header pblock->hashPrevBlock = pindexPrev->GetBlockHash(); UpdateTime(pblock, chainparams.GetConsensus(), pindexPrev); + if (nHeight >= chainparams.GetConsensus().nTimeRulesActivationHeight) { + SyncCoinbaseTimestamp(pblock); + } pblock->nBits = GetNextTargetRequired(pindexPrev); pblock->nNonce = 0; pblocktemplate->vTxSigOpsCost[0] = WITNESS_SCALE_FACTOR * GetLegacySigOpCount(*pblock->vtx[0]); @@ -683,8 +706,15 @@ void Miner(CWallet *pwallet) break; // Update nTime every few seconds - UpdateTime(pblock, Params().GetConsensus(), pindexPrev); - nBlockTime = ByteReverse(pblock->nTime); + int64_t nTimeChange = UpdateTime(pblock, Params().GetConsensus(), pindexPrev); + if (nTimeChange != 0) { + const int nHeight = pindexPrev->nHeight + 1; + if (nHeight >= Params().GetConsensus().nTimeRulesActivationHeight) { + SyncCoinbaseTimestamp(pblock); + FormatHashBuffers(pblock, pmidstate, pdata, phash1); + } + nBlockTime = ByteReverse(pblock->nTime); + } } } } diff --git a/src/miner.h b/src/miner.h index d40dad3b66..8a4c791809 100644 --- a/src/miner.h +++ b/src/miner.h @@ -198,7 +198,8 @@ class BlockAssembler /** Modify the extranonce in a block */ void IncrementExtraNonce(CBlock* pblock, const CBlockIndex* pindexPrev, unsigned int& nExtraNonce); -int64_t UpdateTime(CBlockHeader* pblock, const Consensus::Params& consensusParams, const CBlockIndex* pindexPrev); +int64_t UpdateTime(CBlock* pblock, const Consensus::Params& consensusParams, const CBlockIndex* pindexPrev); +void SyncCoinbaseTimestamp(CBlock* pblock); /** Base sha256 mining transform */ void SHA256Transform(void* pstate, void* pinput, const void* pinit); diff --git a/src/net.cpp b/src/net.cpp index 63b7833822..ebeaa2cbf5 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1585,8 +1585,8 @@ void CConnman::ThreadDNSAddressSeed() continue; } unsigned int nMaxIPs = 256; // Limits number of IPs learned from a DNS seed - if (LookupHost(host.c_str(), vIPs, nMaxIPs, true)) { - for (const CNetAddr& ip : vIPs) { + auto addSeedIPs = [&](const std::vector& ips) { + for (const CNetAddr& ip : ips) { int nOneDay = 24*3600; CAddress addr = CAddress(CService(ip, Params().GetDefaultPort()), requiredServiceBits); addr.nTime = GetTime() - 3*nOneDay - rng.randrange(4*nOneDay); // use a random age between 3 and 7 days old @@ -1594,9 +1594,20 @@ void CConnman::ThreadDNSAddressSeed() found++; } addrman.Add(vAdd, resolveSource); + }; + // Prefer the root hostname first so manually maintained CloudFlare A records + // work without a live dnsseed crawler or cf-uploader. + if (LookupHost(seed.c_str(), vIPs, nMaxIPs, true)) { + LogPrintf("Loading addresses from DNS seed %s\n", seed); + addSeedIPs(vIPs); + } else if (LookupHost(host.c_str(), vIPs, nMaxIPs, true)) { + if (!resolveSource.SetInternal(host)) { + continue; + } + LogPrintf("Loading addresses from DNS seed %s (service-bit subdomain)\n", host); + addSeedIPs(vIPs); } else { - // We now avoid directly using results from DNS Seeds which do not support service bit filtering, - // instead using them as a oneshot to get nodes with our desired service bits. + // Last resort: try connecting to the seed hostname itself. AddOneShot(seed); } } @@ -2159,6 +2170,10 @@ void CConnman::SetNetworkActive(bool active) fNetworkActive = active; + if (!fNetworkActive) { + DisconnectNodes(); + } + uiInterface.NotifyNetworkActiveChanged(fNetworkActive); } diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 77c72ac8aa..3be4fdc1b5 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -1530,10 +1530,6 @@ void static ProcessGetData(CNode* pfrom, const CChainParams& chainparams, CConnm // messages from this peer (likely resulting in our peer eventually // disconnecting us). if (pfrom->m_tx_relay != nullptr) { - // mempool entries added before this time have likely expired from mapRelay - const std::chrono::seconds longlived_mempool_time = GetTime() - RELAY_TX_CACHE_TIME; - const std::chrono::seconds mempool_req = pfrom->m_tx_relay->m_last_mempool_req.load(); - LOCK(cs_main); while (it != pfrom->vRecvGetData.end() && (it->type == MSG_TX || it->type == MSG_WITNESS_TX)) { @@ -1553,16 +1549,11 @@ void static ProcessGetData(CNode* pfrom, const CChainParams& chainparams, CConnm if (mi != mapRelay.end()) { connman->PushMessage(pfrom, msgMaker.Make(nSendFlags, NetMsgType::TX, *mi->second)); push = true; -<<<<<<< HEAD - } else { + } else if (pfrom->m_tx_relay->timeLastMempoolReq) { auto txinfo = mempool.info(inv.hash); // To protect privacy, do not answer getdata using the mempool when - // that TX couldn't have been INVed in reply to a MEMPOOL request, - // or when it's too recent to have expired from mapRelay. - if (txinfo.tx && ( - (mempool_req.count() && txinfo.m_time <= mempool_req) - || (txinfo.m_time <= longlived_mempool_time))) - { + // that TX couldn't have been INVed in reply to a MEMPOOL request. + if (txinfo.tx && txinfo.nTime <= pfrom->m_tx_relay->timeLastMempoolReq) { connman->PushMessage(pfrom, msgMaker.Make(nSendFlags, NetMsgType::TX, *txinfo.tx)); push = true; } @@ -1643,6 +1634,10 @@ bool static ProcessHeadersMessage(CNode *pfrom, CConnman *connman, const std::ve return true; } + if (IsChainSyncPausedForBootstrap()) { + return true; + } + bool received_new_header = false; const CBlockIndex *pindexLast = nullptr; { @@ -3645,7 +3640,7 @@ bool PeerLogicValidation::SendMessages(CNode* pto) if (pindexBestHeader == nullptr) pindexBestHeader = ::ChainActive().Tip(); bool fFetch = state.fPreferredDownload || (nPreferredDownload == 0 && !pto->fClient && !pto->fOneShot); // Download if this is a nice peer, or we have no nice peers and this one might do. - if (!state.fSyncStarted && !pto->fClient && !fImporting && !fReindex) { + if (!IsChainSyncPausedForBootstrap() && !state.fSyncStarted && !pto->fClient && !fImporting && !fReindex) { // Only actively request headers from a single peer, unless we're close to today. if ((nSyncStarted == 0 && fFetch) || pindexBestHeader->GetBlockTime() > GetAdjustedTime() - 24 * 60 * 60) { state.fSyncStarted = true; diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index 45e2f09c95..2a56fa7ba6 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -9,6 +9,12 @@ #include #include +#include +#if ENABLE_DEV_HELPER_WINDOW +#include +#endif +#include + #include #include #include @@ -33,6 +39,7 @@ #include #include #include +#include #include #include @@ -117,10 +124,23 @@ static void initTranslations(QTranslator &qtTranslatorBase, QTranslator &qtTrans QApplication::installTranslator(&translator); } -/* qDebug() message handler --> debug.log */ +/* qDebug() message handler --> debug.log and activity.log */ void DebugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString &msg) { - Q_UNUSED(context); + ActivityLevel level = ActivityLevel::Info; + switch (type) { + case QtDebugMsg: level = ActivityLevel::Debug; break; + case QtWarningMsg: level = ActivityLevel::Warning; break; + case QtCriticalMsg: + case QtFatalMsg: level = ActivityLevel::Error; break; + case QtInfoMsg: level = ActivityLevel::Info; break; + default: break; + } + + const char* file = context.file ? context.file : nullptr; + const char* function = context.function ? context.function : nullptr; + LogActivityEx(level, file, context.line, function, "GUI: %s", msg.toStdString().c_str()); + if (type == QtDebugMsg) { LogPrint(BCLog::QT, "GUI: %s\n", msg.toStdString()); } else { @@ -410,6 +430,9 @@ static void SetupUIArgs() gArgs.AddArg("-resetguisettings", "Reset all settings changed in the GUI", ArgsManager::ALLOW_ANY, OptionsCategory::GUI); gArgs.AddArg("-rootcertificates=", "Set SSL root certificates for payment request (default: -system-)", ArgsManager::ALLOW_ANY, OptionsCategory::GUI); gArgs.AddArg("-splash", strprintf("Show splash screen on startup (default: %u)", DEFAULT_SPLASHSCREEN), ArgsManager::ALLOW_ANY, OptionsCategory::GUI); +#if ENABLE_DEV_HELPER_WINDOW + gArgs.AddArg("-devedition", "Enable Developer Edition branding and tools (requires compile flag and master password for tools)", ArgsManager::ALLOW_ANY, OptionsCategory::GUI); +#endif gArgs.AddArg("-uiplatform", strprintf("Select platform to customize UI for (one of windows, macosx, other; default: %s)", BitcoinGUI::DEFAULT_UIPLATFORM), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::GUI); } @@ -584,6 +607,11 @@ int GuiMain(int argc, char* argv[]) try { app.createWindow(networkStyle.data()); +#if ENABLE_DEV_HELPER_WINDOW + // Blocking prompt before init/bootstrap so trace captures the full startup path. + if (IsDeveloperEditionActive()) + DevTools::OfferStartupTraceWindow(app.getWindow()); +#endif // Perform base initialization before spinning up initialization/shutdown thread // This is acceptable because this function only contains steps that are quick to execute, // so the GUI thread won't be held up. diff --git a/src/qt/bitcoin.h b/src/qt/bitcoin.h index 8c77fd8a7d..cb9d249f5c 100644 --- a/src/qt/bitcoin.h +++ b/src/qt/bitcoin.h @@ -87,6 +87,9 @@ class BitcoinApplication: public QApplication /// Get window identifier of QMainWindow (BitcoinGUI) WId getMainWinId() const; + /// Main window (null before createWindow) + BitcoinGUI* getWindow() const { return window; } + /// Setup platform style void setupPlatformStyle(); diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index c98bb6636f..7b51b6f7d5 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -22,6 +22,13 @@ #include #include +#include +#include +#include + +#include +#include + #ifdef ENABLE_WALLET #include @@ -273,12 +280,6 @@ BitcoinGUI::BitcoinGUI(interfaces::Node& node, const PlatformStyle *_platformSty QTextStream ts(&f); setStyleSheet(ts.readAll()); f.close(); - - // Force Check for update - if(needClientUpdate()) { - auto updatedialog = new UpdateDialog(this); - updatedialog->exec(); - } } BitcoinGUI::~BitcoinGUI() @@ -746,6 +747,9 @@ void BitcoinGUI::setClientModel(ClientModel *_clientModel) updateProxyIcon(); + // Defer network-heavy startup prompts so headers/IBD are not blocked on the UI thread. + QTimer::singleShot(500, this, &BitcoinGUI::deferredStartupChecks); + #ifdef ENABLE_WALLET if(walletFrame) { @@ -797,6 +801,10 @@ void BitcoinGUI::setWalletController(WalletController* wallet_controller) connect(wallet_controller, &WalletController::walletAdded, this, &BitcoinGUI::addWallet); connect(wallet_controller, &WalletController::walletRemoved, this, &BitcoinGUI::removeWallet); + if (walletFrame) { + walletFrame->setWalletController(wallet_controller); + } + for (WalletModel* wallet_model : m_wallet_controller->getOpenWallets()) { addWallet(wallet_model); } @@ -1019,6 +1027,65 @@ void BitcoinGUI::bootstrapClicked() bootstrapdialog->exec(); } +void BitcoinGUI::deferredStartupChecks() +{ + if (!clientModel) + return; + +#if ENABLE_DEV_HELPER_WINDOW + if (IsDeveloperEditionActive() && !IsDevStartupPromptComplete()) + return; +#endif + + checkForBootStrap(); + + QThread* update_thread = new QThread; + QObject* update_worker = new QObject; + update_worker->moveToThread(update_thread); + connect(update_thread, &QThread::started, update_worker, [this, update_thread]() { + bool needUpdate = false; + try { + needUpdate = needClientUpdate(); + } catch (...) { + update_thread->quit(); + return; + } + if (needUpdate) { + QTimer::singleShot(0, this, [this]() { + if (!clientModel) + return; + auto* updatedialog = new UpdateDialog(this); + updatedialog->setAttribute(Qt::WA_DeleteOnClose); + updatedialog->show(); + }); + } + update_thread->quit(); + }); + connect(update_thread, &QThread::finished, update_worker, &QObject::deleteLater); + connect(update_thread, &QThread::finished, update_thread, &QObject::deleteLater); + update_thread->start(); +} + +void BitcoinGUI::checkForBootStrap() +{ +#if ENABLE_DEV_HELPER_WINDOW + if (IsDeveloperEditionActive() && !IsDevStartupPromptComplete()) + return; +#endif + + QDateTime blockDate = QDateTime::fromTime_t(m_node.getLastBlockTime()); + QDateTime currentDate = QDateTime::currentDateTime(); + + if( blockDate.secsTo(currentDate) > 60 * 60 * 24 * 60 ) { + + LogActivityEx(ActivityLevel::Info, __FILE__, __LINE__, __func__, + "Offering bootstrap download (chain more than 60 days behind)"); + + BootstrapDialog bootstrapDialog(this); + bootstrapDialog.exec(); + } +} + void BitcoinGUI::updateClicked() { auto updatedialog = new UpdateDialog(this); @@ -1482,7 +1549,15 @@ void BitcoinGUI::updateProxyIcon() void BitcoinGUI::updateWindowTitle() { - QString window_title = PACKAGE_NAME; + QString window_title; +#if ENABLE_DEV_HELPER_WINDOW + if (IsDeveloperEditionActive()) { + window_title = QString::fromStdString(GetDeveloperEditionTitle()); + } else +#endif + { + window_title = PACKAGE_NAME; + } #ifdef ENABLE_WALLET if (walletFrame) { WalletModel* const wallet_model = walletFrame->currentWalletModel(); diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 1cafd99328..ed0153e36d 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -207,6 +207,10 @@ class BitcoinGUI : public QMainWindow /** Open the OptionsDialog on the specified tab index */ void openOptionsDialogWithTab(OptionsDialog::Tab tab); + /** Deferred bootstrap/update prompts after client model is ready. */ + void deferredStartupChecks(); + void checkForBootStrap(); + Q_SIGNALS: /** Signal raised when a URI was entered or dragged to the GUI */ void receivedURI(const QString &uri); diff --git a/src/qt/bootstrapdialog.cpp b/src/qt/bootstrapdialog.cpp index 87e48c81f9..d4bb9d82a6 100644 --- a/src/qt/bootstrapdialog.cpp +++ b/src/qt/bootstrapdialog.cpp @@ -1,58 +1,453 @@ #include #include #include -#include #include -#include + +#include +#include +#include +#include +#include +#include +#include + +static BootstrapWorker* g_bootstrap_worker = nullptr; + +static void xfer_callback(curl_off_t now, curl_off_t total) +{ + if (g_bootstrap_worker == nullptr) + return; + + QMetaObject::invokeMethod(g_bootstrap_worker, "reportProgress", Qt::QueuedConnection, + Q_ARG(qint64, static_cast(total)), + Q_ARG(qint64, static_cast(now))); +} + +void BootstrapWorker::reportProgress(qint64 total, qint64 now) +{ + Q_EMIT progress(total, now); +} + +void BootstrapWorker::run() +{ + g_bootstrap_worker = this; + set_xferinfo_data(reinterpret_cast(xfer_callback)); + set_bootstrap_status_fn([this](const char* msg) { + Q_EMIT statusMessage(QString::fromUtf8(msg)); + }); + + try { + downloadBootstrap(); + set_bootstrap_status_fn(nullptr); + set_xferinfo_data(nullptr); + g_bootstrap_worker = nullptr; + Q_EMIT finished(Success, QString()); + } catch (const std::runtime_error& e) { + set_bootstrap_status_fn(nullptr); + set_xferinfo_data(nullptr); + g_bootstrap_worker = nullptr; + const QString error = QString::fromStdString(e.what()); + if (download_cancelled()) { + Q_EMIT finished(Cancelled, error); + } else { + Q_EMIT finished(Failed, error); + } + } catch (...) { + set_bootstrap_status_fn(nullptr); + set_xferinfo_data(nullptr); + g_bootstrap_worker = nullptr; + if (download_cancelled()) { + Q_EMIT finished(Cancelled, tr("Download cancelled.")); + } else { + Q_EMIT finished(Failed, tr("Unknown bootstrap error.")); + } + } +} BootstrapDialog::BootstrapDialog(QWidget *parent) : QDialog(parent), - ui(new Ui::BootstrapDialog) + ui(new Ui::BootstrapDialog), + m_worker(new BootstrapWorker), + m_downloading(false), + m_worker_running(false), + m_phase(PhaseDownload), + m_lastProgressNow(0), + m_lastProgressTotal(0) { ui->setupUi(this); - setWindowFlags(Qt::Window | Qt::FramelessWindowHint); - setWindowTitle(tr("Chain Bootstrap")); - ui->checkBox->setVisible(false); + setObjectName("BootstrapDialog"); + setWindowTitle(tr("Bootstrap Blockchain")); + setModal(true); + setAttribute(Qt::WA_DeleteOnClose, false); + + QFont titleFont = ui->idleTitle->font(); + titleFont.setPointSize(titleFont.pointSize() + 4); + titleFont.setBold(true); + ui->idleTitle->setFont(titleFont); + ui->progressTitle->setFont(titleFont); + ui->interruptedTitle->setFont(titleFont); + + QFont pctFont = ui->percentLabel->font(); + pctFont.setPointSize(pctFont.pointSize() + 8); + pctFont.setBold(true); + ui->percentLabel->setFont(pctFont); + + ui->interruptedUrlLabel->setText(QString::fromStdString(getBootstrapDownloadUrl())); + ui->detailsText->setPlainText(QString::fromStdString(getBootstrapDownloadUrl())); + ui->interruptedDetailsText->setPlainText(QString::fromStdString(getBootstrapDownloadUrl())); + + QFile styleFile(QStringLiteral(":/style")); + if (styleFile.open(QFile::ReadOnly | QFile::Text)) { + setStyleSheet(QString::fromUtf8(styleFile.readAll())); + } + + m_worker->moveToThread(&m_worker_thread); + connect(m_worker, &BootstrapWorker::progress, this, &BootstrapDialog::onDownloadProgress, Qt::QueuedConnection); + connect(m_worker, &BootstrapWorker::statusMessage, this, &BootstrapDialog::onStatusMessage, Qt::QueuedConnection); + connect(m_worker, &BootstrapWorker::finished, this, &BootstrapDialog::onDownloadFinished, Qt::QueuedConnection); + m_worker_thread.start(); + + setIdleState(); } BootstrapDialog::~BootstrapDialog() { + if (m_downloading) { + set_download_cancelled(true); + } + m_worker_thread.quit(); + m_worker_thread.wait(); + delete m_worker; delete ui; + if (!bootstrapApplyPending()) { + restoreNetworkAfterBootstrap(); + } } -BootstrapDialog* bootstrap_callback_instance; -static void xfer_callback(curl_off_t total, curl_off_t now) +void BootstrapDialog::reject() { - bootstrap_callback_instance->setProgress(total, now); + restoreNetworkAfterBootstrap(); + clearBootstrapPartial(); + QDialog::reject(); } -void BootstrapDialog::on_startButton_clicked() +void BootstrapDialog::closeEvent(QCloseEvent* event) { - extern void set_xferinfo_data(void*); + if (m_downloading) { + event->ignore(); + return; + } + QDialog::closeEvent(event); +} - bootstrap_callback_instance = this; - set_xferinfo_data((void*)xfer_callback); +void BootstrapDialog::setNetworkPausedBadgeVisible(bool visible) +{ + ui->networkPausedBadge->setVisible(visible); +} - QMessageBox::information(this, "Bootstrap", "The client will now bootstrap the chain. \n\nThe Verium vault will exit after extracting the bootstrap and need to be restarted.", QMessageBox::Ok, QMessageBox::Ok); - try { - downloadBootstrap(); - } catch (const std::runtime_error& e) { - QMessageBox::critical(this, tr("Bootstrap failed"), e.what()); +void BootstrapDialog::updateStepper(Phase phase) +{ + m_phase = phase; + + auto applyStep = [&](QLabel* label, Phase stepPhase) { + const char* state = "pending"; + if (phase > stepPhase) { + state = "done"; + } else if (phase == stepPhase) { + state = "active"; + } + label->setProperty("bootstrapStep", state); + label->style()->unpolish(label); + label->style()->polish(label); + label->update(); + }; + + applyStep(ui->stepDownload, PhaseDownload); + applyStep(ui->stepExtract, PhaseExtract); + applyStep(ui->stepValidate, PhaseValidate); +} + +void BootstrapDialog::appendDetails(const QString& line) +{ + ui->detailsText->appendPlainText(line); + ui->interruptedDetailsText->appendPlainText(line); +} + +void BootstrapDialog::setIdleState() +{ + m_downloading = false; + m_worker_running = false; + setNetworkPausedBadgeVisible(false); + ui->stackedWidget->setCurrentIndex(PageIdle); + + const uint64_t partial = getBootstrapPartialBytes(); + if (partial > 0) { + const double mbPartial = static_cast(partial) / (1024.0 * 1024.0); + ui->idlePartialNote->setText(tr("Partial download found (%1 MB). Download will resume from the saved archive.") + .arg(mbPartial, 0, 'f', 1)); + ui->idlePartialNote->setVisible(true); + ui->startButton->setText(tr("Resume download")); + } else { + ui->idlePartialNote->hide(); + ui->startButton->setText(tr("Download bootstrap")); + } + ui->startButton->setEnabled(true); + ui->closeButton->setEnabled(true); +} + +void BootstrapDialog::setProgressState() +{ + m_downloading = true; + setNetworkPausedBadgeVisible(true); + ui->stackedWidget->setCurrentIndex(PageProgress); + ui->detailsText->clear(); + ui->detailsText->appendPlainText(QString::fromStdString(getBootstrapDownloadUrl())); + ui->detailsText->setVisible(false); + ui->showDetailsButton->setText(tr("Show details")); + ui->progressBar->setMinimum(0); + ui->progressBar->setMaximum(0); + ui->progressBar->setValue(0); + ui->percentLabel->setText(QStringLiteral("…")); + ui->sizeLabel->clear(); + updateStepper(PhaseDownload); + raise(); + activateWindow(); +} + +void BootstrapDialog::setInterruptedState(const QString& detail, const QString& progressSummary, int percent) +{ + ui->interruptedDetailLabel->setText(detail); + ui->interruptedProgressLabel->setText(progressSummary); + if (percent >= 0) { + ui->interruptedProgressBar->setMaximum(100); + ui->interruptedProgressBar->setValue(percent); + } else if (m_lastProgressTotal > 0) { + const int pct = static_cast(qMin((m_lastProgressNow * 100) / m_lastProgressTotal, static_cast(100))); + ui->interruptedProgressBar->setMaximum(100); + ui->interruptedProgressBar->setValue(pct); + } else { + ui->interruptedProgressBar->setMaximum(0); + } + ui->interruptedDetailsText->setVisible(false); + ui->interruptedDetailsButton->setText(tr("Details")); + ui->stackedWidget->setCurrentIndex(PageInterrupted); + raise(); + activateWindow(); +} + +void BootstrapDialog::updateProgressMetrics(qint64 total, qint64 now) +{ + m_lastProgressNow = now; + m_lastProgressTotal = total; + + if (total > 0) { + const int pct = static_cast(qMin((now * 100) / total, static_cast(100))); + ui->progressBar->setMaximum(100); + ui->progressBar->setValue(pct); + ui->percentLabel->setText(tr("%1%").arg(pct)); + const double mbNow = static_cast(now) / (1024.0 * 1024.0); + const double mbTotal = static_cast(total) / (1024.0 * 1024.0); + ui->sizeLabel->setText(tr("%1 MB of %2 MB").arg(mbNow, 0, 'f', 1).arg(mbTotal, 0, 'f', 1)); + } else if (now > 0) { + ui->progressBar->setMaximum(0); + ui->percentLabel->setText(QStringLiteral("…")); + const double mbNow = static_cast(now) / (1024.0 * 1024.0); + ui->sizeLabel->setText(tr("%1 MB downloaded").arg(mbNow, 0, 'f', 1)); + } +} + +void BootstrapDialog::startDownload() +{ + if (m_worker_running) { + return; } - set_xferinfo_data(nullptr); - bootstrap_callback_instance = nullptr; - this->close(); - QApplication::quit(); + + reset_download_cancel(); + m_worker_running = true; + pauseNetworkForBootstrap(); + setProgressState(); + QMetaObject::invokeMethod(m_worker, "run", Qt::QueuedConnection); +} + +void BootstrapDialog::on_startButton_clicked() +{ + startDownload(); } void BootstrapDialog::on_closeButton_clicked() { - this->close(); + reject(); } -void BootstrapDialog::setProgress(curl_off_t total, curl_off_t now) +void BootstrapDialog::on_cancelDownloadButton_clicked() { - ui->progressBar->setMinimum(0); - ui->progressBar->setMaximum(total - 1); - ui->progressBar->setValue(now); + if (!m_downloading) { + reject(); + return; + } + set_download_cancelled(true); + ui->cancelDownloadButton->setEnabled(false); + ui->progressTitle->setText(tr("Cancelling…")); +} + +void BootstrapDialog::on_showDetailsButton_clicked() +{ + const bool show = !ui->detailsText->isVisible(); + ui->detailsText->setVisible(show); + ui->showDetailsButton->setText(show ? tr("Hide details") : tr("Show details")); +} + +void BootstrapDialog::on_interruptedRetryButton_clicked() +{ + if (m_worker_running) { + requestBootstrapDownloadRetryNow(); + ui->interruptedRetryButton->setEnabled(false); + ui->interruptedDetailLabel->setText(tr("Retrying now…")); + setProgressState(); + return; + } + + startDownload(); +} + +void BootstrapDialog::on_interruptedCancelButton_clicked() +{ + if (m_worker_running) { + set_download_cancelled(true); + ui->interruptedCancelButton->setEnabled(false); + return; + } + reject(); +} + +void BootstrapDialog::on_interruptedDetailsButton_clicked() +{ + const bool show = !ui->interruptedDetailsText->isVisible(); + ui->interruptedDetailsText->setVisible(show); + ui->interruptedDetailsButton->setText(show ? tr("Hide details") : tr("Details")); +} + +void BootstrapDialog::onStatusMessage(const QString& message) +{ + appendDetails(message); + + if (message.contains(QStringLiteral("Download interrupted"), Qt::CaseInsensitive) || + message.contains(QStringLiteral("Retrying in"), Qt::CaseInsensitive)) { + QString progressSummary; + if (m_lastProgressTotal > 0) { + const double mbNow = static_cast(m_lastProgressNow) / (1024.0 * 1024.0); + const double mbTotal = static_cast(m_lastProgressTotal) / (1024.0 * 1024.0); + progressSummary = tr("Last progress: %1 MB of %2 MB").arg(mbNow, 0, 'f', 1).arg(mbTotal, 0, 'f', 1); + } else if (m_lastProgressNow > 0) { + const double mbNow = static_cast(m_lastProgressNow) / (1024.0 * 1024.0); + progressSummary = tr("Last progress: %1 MB downloaded").arg(mbNow, 0, 'f', 1); + } else { + const uint64_t partial = getBootstrapPartialBytes(); + if (partial > 0) { + progressSummary = tr("Last progress: %1 MB saved on disk") + .arg(static_cast(partial) / (1024.0 * 1024.0), 0, 'f', 1); + } + } + setInterruptedState(tr("Connection lost while downloading"), progressSummary); + ui->interruptedRetryButton->setEnabled(true); + return; + } + + if (message.startsWith(QStringLiteral("Extracting"), Qt::CaseInsensitive)) { + ui->stackedWidget->setCurrentIndex(PageProgress); + ui->progressTitle->setText(tr("Extracting bootstrap archive")); + ui->progressSubtitle->setText(tr("Unpacking chain data to the staging folder. This may take several minutes.")); + ui->progressBar->setMaximum(0); + ui->percentLabel->setText(QStringLiteral("…")); + ui->sizeLabel->setText(message); + updateStepper(PhaseExtract); + return; + } + + if (message.contains(QStringLiteral("files processed"), Qt::CaseInsensitive)) { + ui->sizeLabel->setText(message); + return; + } + + if (message.startsWith(QStringLiteral("Validating"), Qt::CaseInsensitive)) { + ui->progressTitle->setText(tr("Validating bootstrap archive")); + ui->progressSubtitle->setText(tr("Checking blocks and chainstate.")); + ui->progressBar->setMaximum(0); + ui->percentLabel->setText(QStringLiteral("…")); + ui->sizeLabel->clear(); + updateStepper(PhaseValidate); + return; + } + + if (message.startsWith(QStringLiteral("Downloading"), Qt::CaseInsensitive) || + message.startsWith(QStringLiteral("Resuming download"), Qt::CaseInsensitive)) { + ui->stackedWidget->setCurrentIndex(PageProgress); + ui->progressTitle->setText(tr("Downloading bootstrap archive")); + ui->progressSubtitle->setText(tr("Downloading from files.vericonomy.com over HTTPS.")); + updateStepper(PhaseDownload); + return; + } + + if (message.startsWith(QStringLiteral("Bootstrap ready"), Qt::CaseInsensitive)) { + ui->progressTitle->setText(tr("Bootstrap ready")); + ui->progressSubtitle->setText(tr("Verium will now shut down. Restart to install chain data and finish syncing.")); + ui->progressBar->setMaximum(100); + ui->progressBar->setValue(100); + ui->percentLabel->setText(tr("100%")); + updateStepper(PhaseComplete); + } +} + +void BootstrapDialog::onDownloadProgress(qint64 total, qint64 now) +{ + ui->stackedWidget->setCurrentIndex(PageProgress); + if (m_phase <= PhaseDownload) { + updateStepper(PhaseDownload); + } + updateProgressMetrics(total, now); +} + +void BootstrapDialog::onDownloadFinished(int result, const QString& error) +{ + m_downloading = false; + m_worker_running = false; + ui->cancelDownloadButton->setEnabled(true); + ui->interruptedCancelButton->setEnabled(true); + ui->interruptedRetryButton->setEnabled(true); + + switch (result) { + case BootstrapWorker::Success: + ui->progressTitle->setText(tr("Bootstrap ready")); + ui->progressSubtitle->setText(tr("Verium will now shut down.\nRestart to install chain data and finish syncing.")); + ui->progressBar->setMaximum(100); + ui->progressBar->setValue(100); + ui->percentLabel->setText(tr("100%")); + updateStepper(PhaseComplete); + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + accept(); + QApplication::quit(); + break; + case BootstrapWorker::Cancelled: { + m_lastError = error; + QString progressSummary; + if (m_lastProgressTotal > 0) { + const double mbNow = static_cast(m_lastProgressNow) / (1024.0 * 1024.0); + const double mbTotal = static_cast(m_lastProgressTotal) / (1024.0 * 1024.0); + progressSummary = tr("Last progress: %1 MB of %2 MB").arg(mbNow, 0, 'f', 1).arg(mbTotal, 0, 'f', 1); + } else { + const uint64_t partial = getBootstrapPartialBytes(); + progressSummary = tr("Last progress: %1 MB saved on disk") + .arg(static_cast(partial) / (1024.0 * 1024.0), 0, 'f', 1); + } + setInterruptedState(tr("Download cancelled"), progressSummary); + appendDetails(error); + break; + } + case BootstrapWorker::Failed: + default: + m_lastError = error; + setInterruptedState(tr("Download stopped"), error); + appendDetails(error); + break; + } } diff --git a/src/qt/bootstrapdialog.h b/src/qt/bootstrapdialog.h index e23770ef42..db5debd837 100644 --- a/src/qt/bootstrapdialog.h +++ b/src/qt/bootstrapdialog.h @@ -2,29 +2,94 @@ #define BITCOIN_QT_BOOTSTRAPDIALOG_H #include -#include -#include #include -#include +#include +#include + +class QCloseEvent; namespace Ui { class BootstrapDialog; } + +class BootstrapWorker : public QObject +{ + Q_OBJECT + +public: + enum Result { + Success, + Failed, + Cancelled + }; + +Q_SIGNALS: + void progress(qint64 total, qint64 now); + void statusMessage(const QString& message); + void finished(int result, const QString& error); + +public Q_SLOTS: + void reportProgress(qint64 total, qint64 now); + void run(); +}; + +/** Dialog offering bootstrap download with retry and normal sync fallback. */ class BootstrapDialog : public QDialog { Q_OBJECT public: + enum Page { + PageIdle = 0, + PageProgress = 1, + PageInterrupted = 2 + }; + + enum Phase { + PhaseDownload = 0, + PhaseExtract = 1, + PhaseValidate = 2, + PhaseComplete = 3 + }; + explicit BootstrapDialog(QWidget *parent = 0); ~BootstrapDialog(); - void setProgress(curl_off_t, curl_off_t); Ui::BootstrapDialog *ui; private Q_SLOTS: - void on_startButton_clicked(); void on_closeButton_clicked(); + void on_cancelDownloadButton_clicked(); + void on_showDetailsButton_clicked(); + void on_interruptedRetryButton_clicked(); + void on_interruptedCancelButton_clicked(); + void on_interruptedDetailsButton_clicked(); + void onDownloadProgress(qint64 total, qint64 now); + void onStatusMessage(const QString& message); + void onDownloadFinished(int result, const QString& error); + +private: + void setIdleState(); + void setProgressState(); + void setInterruptedState(const QString& detail, const QString& progressSummary, int percent = -1); + void startDownload(); + void updateStepper(Phase phase); + void updateProgressMetrics(qint64 total, qint64 now); + void appendDetails(const QString& line); + void setNetworkPausedBadgeVisible(bool visible); + void reject() override; + void closeEvent(QCloseEvent* event) override; + + QThread m_worker_thread; + BootstrapWorker* m_worker; + bool m_downloading; + bool m_worker_running; + Phase m_phase; + qint64 m_lastProgressNow; + qint64 m_lastProgressTotal; + QString m_lastError; }; + #endif // BITCOIN_QT_BOOTSTRAPDIALOG_H diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index 5b216b2705..20094a741d 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include @@ -154,6 +156,10 @@ BanTableModel *ClientModel::getBanTableModel() QString ClientModel::formatFullVersion() const { +#if ENABLE_DEV_HELPER_WINDOW + if (IsDeveloperEditionActive()) + return QString::fromStdString(GetDeveloperEditionVersionString()); +#endif return QString::fromStdString(FormatFullVersion()); } diff --git a/src/qt/devhelperwindow.cpp b/src/qt/devhelperwindow.cpp new file mode 100644 index 0000000000..4a9aa07a78 --- /dev/null +++ b/src/qt/devhelperwindow.cpp @@ -0,0 +1,375 @@ +// Copyright (c) 2026 The Verium developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#if ENABLE_DEV_HELPER_WINDOW + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +Q_DECLARE_METATYPE(ActivityEvent) + +DevHelperWindow* DevHelperWindow::s_instance = nullptr; + +DevHelperWindow::DevHelperWindow(QWidget* parent) : + QWidget(parent, Qt::Window) +{ + setWindowTitle(tr("Verium Verbose Dev Trace")); + setMinimumSize(720, 420); + resize(960, 560); + + auto* layout = new QVBoxLayout(this); + + auto* header = new QLabel(tr( + "Super-verbose trace for copy/paste debugging. Every init step, progress dialog, GUI message, " + "and debug.log line is recorded with sequence numbers and source locations.\n" + "Use \"Copy incident report\" after a hang/error to capture what happened leading up to it.")); + header->setWordWrap(true); + layout->addWidget(header); + + m_logView = new QPlainTextEdit(); + m_logView->setReadOnly(true); + m_logView->setLineWrapMode(QPlainTextEdit::NoWrap); + QFont mono = QFontDatabase::systemFont(QFontDatabase::FixedFont); + mono.setPointSize(mono.pointSize() - 1); + m_logView->setFont(mono); + layout->addWidget(m_logView, 1); + + m_statusLabel = new QLabel(); + layout->addWidget(m_statusLabel); + + auto* toolbar = new QHBoxLayout(); + m_errorsOnlyCheck = new QCheckBox(tr("Errors/warnings only")); + m_alwaysOnTopCheck = new QCheckBox(tr("Always on top")); + m_raiseOnErrorCheck = new QCheckBox(tr("Raise on error")); + m_raiseOnErrorCheck->setChecked(true); + m_clearButton = new QPushButton(tr("Clear view")); + m_copyAllButton = new QPushButton(tr("Copy all")); + m_copyLastButton = new QPushButton(tr("Copy last 200")); + m_copyIncidentButton = new QPushButton(tr("Copy incident report")); + m_selectAllButton = new QPushButton(tr("Select all")); + m_openLogButton = new QPushButton(tr("Open activity.log")); + + m_copyIncidentButton->setToolTip(tr("Paste-ready report with the last ~100 events and error context")); + m_copyLastButton->setToolTip(tr("Copy the most recent lines from this window")); + + toolbar->addWidget(m_errorsOnlyCheck); + toolbar->addWidget(m_alwaysOnTopCheck); + toolbar->addWidget(m_raiseOnErrorCheck); + toolbar->addStretch(1); + toolbar->addWidget(m_copyIncidentButton); + toolbar->addWidget(m_copyLastButton); + toolbar->addWidget(m_copyAllButton); + toolbar->addWidget(m_selectAllButton); + toolbar->addWidget(m_clearButton); + toolbar->addWidget(m_openLogButton); + layout->addLayout(toolbar); + + connect(m_errorsOnlyCheck, &QCheckBox::toggled, this, &DevHelperWindow::toggleErrorsOnly); + connect(m_alwaysOnTopCheck, &QCheckBox::toggled, this, &DevHelperWindow::toggleAlwaysOnTop); + connect(m_clearButton, &QPushButton::clicked, this, &DevHelperWindow::clearLog); + connect(m_copyAllButton, &QPushButton::clicked, this, &DevHelperWindow::copyAll); + connect(m_copyLastButton, &QPushButton::clicked, this, &DevHelperWindow::copyLastLines); + connect(m_copyIncidentButton, &QPushButton::clicked, this, &DevHelperWindow::copyIncidentReport); + connect(m_selectAllButton, &QPushButton::clicked, this, &DevHelperWindow::selectAll); + connect(m_openLogButton, &QPushButton::clicked, this, &DevHelperWindow::openLogFile); + + qRegisterMetaType("ActivityEvent"); + m_event_connection = ActivityEventSignals().connect([this](const ActivityEvent& event) { + QMetaObject::invokeMethod(this, "deliverActivityEvent", Qt::QueuedConnection, + Q_ARG(ActivityEvent, event)); + }); + + QTimer::singleShot(0, this, [this]() { loadRecentHistory(); }); + s_instance = this; + updateStatusBar(); + + LogActivityEx(ActivityLevel::Info, __FILE__, __LINE__, __func__, + "Verbose dev trace window opened (%s)", GetDeveloperEditionTitle().c_str()); +} + +DevHelperWindow::~DevHelperWindow() +{ + if (s_instance == this) + s_instance = nullptr; +} + +DevHelperWindow* DevHelperWindow::CreateAndShow(QWidget* parent) +{ + if (s_instance) { + s_instance->show(); + s_instance->raise(); + s_instance->activateWindow(); + if (parent) { + const QRect mainGeo = parent->frameGeometry(); + s_instance->move(mainGeo.right() + 12, mainGeo.top()); + } + return s_instance; + } + + auto* window = new DevHelperWindow(parent); + window->setAttribute(Qt::WA_DeleteOnClose, false); + if (parent) { + const QRect mainGeo = parent->frameGeometry(); + window->move(mainGeo.right() + 12, mainGeo.top()); + } + window->show(); + window->raise(); + return window; +} + +void DevHelperWindow::setPinnedForSession(bool pinned) +{ + m_pinnedForSession = pinned; + if (pinned) { + setWindowTitle(tr("Verium Verbose Dev Trace (session)")); + LogActivityEx(ActivityLevel::Info, __FILE__, __LINE__, __func__, + "Verbose dev trace pinned for session"); + } +} + +void DevHelperWindow::closeEvent(QCloseEvent* event) +{ + if (m_pinnedForSession) { + event->ignore(); + m_statusLabel->setText(tr("Pinned for this session — restart Verium to open dev tools again.")); + raise(); + activateWindow(); + return; + } + QWidget::closeEvent(event); +} + +DevHelperWindow* DevHelperWindow::instance() +{ + return s_instance; +} + +bool DevHelperWindow::shouldShow(ActivityLevel level) const +{ + if (!m_errorsOnlyCheck || !m_errorsOnlyCheck->isChecked()) + return true; + return level == ActivityLevel::Error || level == ActivityLevel::Warning; +} + +void DevHelperWindow::appendFormattedLine(const QString& line, ActivityLevel level, bool is_error) +{ + if (is_error) + ++m_error_count; + ++m_visible_count; + + m_allLines.append(line); + while (m_allLines.size() > kMaxStoredLines) + m_allLines.removeFirst(); + + QTextCharFormat fmt; + switch (level) { + case ActivityLevel::Error: + fmt.setForeground(Qt::red); + fmt.setFontWeight(QFont::Bold); + break; + case ActivityLevel::Warning: + fmt.setForeground(QColor(200, 100, 0)); + break; + case ActivityLevel::Progress: + fmt.setForeground(QColor(0, 80, 180)); + break; + case ActivityLevel::Debug: + fmt.setForeground(QColor(90, 90, 90)); + break; + default: + fmt.setForeground(palette().text().color()); + break; + } + + m_logView->moveCursor(QTextCursor::End); + m_logView->setCurrentCharFormat(fmt); + m_logView->insertPlainText(line + "\n"); + m_logView->moveCursor(QTextCursor::End); + + updateStatusBar(); +} + +void DevHelperWindow::deliverActivityEvent(ActivityEvent event) +{ + queueEvent(std::move(event)); +} + +void DevHelperWindow::queueEvent(ActivityEvent event) +{ + std::lock_guard lock(m_pendingMutex); + if (m_pendingEvents.size() > 3000 && event.level == ActivityLevel::Debug) + return; + m_pendingEvents.push_back(std::move(event)); + if (m_pendingEvents.size() > 2000) + m_pendingEvents.erase(m_pendingEvents.begin(), m_pendingEvents.begin() + 1000); + + if (m_flushScheduled) + return; + m_flushScheduled = true; + QTimer::singleShot(50, this, [this]() { + m_flushScheduled = false; + flushPendingEvents(); + }); +} + +void DevHelperWindow::flushPendingEvents() +{ + std::vector batch; + { + std::lock_guard lock(m_pendingMutex); + if (m_pendingEvents.empty()) + return; + batch.swap(m_pendingEvents); + } + + m_logView->setUpdatesEnabled(false); + for (const ActivityEvent& event : batch) { + appendEvent(event); + } + m_logView->setUpdatesEnabled(true); + m_logView->moveCursor(QTextCursor::End); +} + +void DevHelperWindow::appendEvent(const ActivityEvent& event) +{ + if (!shouldShow(event.level)) + return; + + const QString line = QString::fromStdString(FormatActivityEventLine(event)); + appendFormattedLine(line, event.level, event.level == ActivityLevel::Error); + + if (event.level == ActivityLevel::Error) { + setWindowTitle(tr("Verium Verbose Dev Trace — %1 error(s) — last #%2") + .arg(m_error_count) + .arg(static_cast(event.sequence))); + if (m_raiseOnErrorCheck && m_raiseOnErrorCheck->isChecked()) { + show(); + raise(); + activateWindow(); + } + } +} + +void DevHelperWindow::loadRecentHistory() +{ + const fs::path path = GetActivityLogPath(); + if (!fs::exists(path)) + return; + + FILE* f = fsbridge::fopen(path, "r"); + if (!f) + return; + + fseek(f, 0, SEEK_END); + const long size = ftell(f); + const long start = size > 512000 ? size - 512000 : 0; + fseek(f, start, SEEK_SET); + + char buf[8192]; + size_t n; + QString chunk; + while ((n = fread(buf, 1, sizeof(buf), f)) > 0) { + chunk += QString::fromUtf8(buf, static_cast(n)); + } + fclose(f); + + const QStringList lines = chunk.split('\n', QString::SkipEmptyParts); + const int tail = qMin(lines.size(), 100); + for (int i = lines.size() - tail; i < lines.size(); ++i) { + m_allLines.append(lines.at(i)); + m_logView->appendPlainText(lines.at(i)); + ++m_visible_count; + } + m_logView->moveCursor(QTextCursor::End); +} + +void DevHelperWindow::updateStatusBar() +{ + m_statusLabel->setText(tr("Visible: %1 | Stored: %2 | Total events: %3 | Last error: #%4") + .arg(m_visible_count) + .arg(m_allLines.size()) + .arg(static_cast(GetActivityEventCount())) + .arg(static_cast(GetLastErrorSequence()))); +} + +void DevHelperWindow::clearLog() +{ + m_logView->clear(); + m_allLines.clear(); + m_error_count = 0; + m_visible_count = 0; + setWindowTitle(tr("Verium Verbose Dev Trace")); + updateStatusBar(); +} + +void DevHelperWindow::copyAll() +{ + QApplication::clipboard()->setText(m_logView->toPlainText()); +} + +void DevHelperWindow::copyLastLines() +{ + const int n = qMin(kCopyLastLines, m_allLines.size()); + if (n <= 0) + return; + QApplication::clipboard()->setText(m_allLines.mid(m_allLines.size() - n).join('\n')); +} + +void DevHelperWindow::copyIncidentReport() +{ + QApplication::clipboard()->setText(QString::fromStdString(BuildIncidentReport(100))); +} + +void DevHelperWindow::selectAll() +{ + m_logView->selectAll(); + m_logView->setFocus(); +} + +void DevHelperWindow::openLogFile() +{ + GUIUtil::openActivityLogfile(); +} + +void DevHelperWindow::toggleAlwaysOnTop(bool checked) +{ + Qt::WindowFlags flags = windowFlags(); + if (checked) + flags |= Qt::WindowStaysOnTopHint; + else + flags &= ~Qt::WindowStaysOnTopHint; + setWindowFlags(flags); + show(); +} + +void DevHelperWindow::toggleErrorsOnly(bool checked) +{ + Q_UNUSED(checked); +} + +#endif // ENABLE_DEV_HELPER_WINDOW diff --git a/src/qt/devhelperwindow.h b/src/qt/devhelperwindow.h new file mode 100644 index 0000000000..e70135e5a3 --- /dev/null +++ b/src/qt/devhelperwindow.h @@ -0,0 +1,90 @@ +// Copyright (c) 2026 The Vericoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_DEVHELPERWINDOW_H +#define BITCOIN_QT_DEVHELPERWINDOW_H + +#include + +#if ENABLE_DEV_HELPER_WINDOW + +#include + +#include +#include + +#include + +class QCloseEvent; +class QCheckBox; +class QPlainTextEdit; +class QPushButton; +class QLabel; + +/** Live verbose dev trace window — paste-ready incident reports. */ +class DevHelperWindow : public QWidget +{ + Q_OBJECT + +public: + explicit DevHelperWindow(QWidget* parent = nullptr); + ~DevHelperWindow(); + + static DevHelperWindow* CreateAndShow(QWidget* parent = nullptr); + static DevHelperWindow* instance(); + + void setPinnedForSession(bool pinned); + bool isPinnedForSession() const { return m_pinnedForSession; } + +public Q_SLOTS: + void deliverActivityEvent(ActivityEvent event); + void appendEvent(const ActivityEvent& event); + void clearLog(); + void copyAll(); + void copyLastLines(); + void copyIncidentReport(); + void selectAll(); + void openLogFile(); + void toggleAlwaysOnTop(bool checked); + void toggleErrorsOnly(bool checked); + +protected: + void closeEvent(QCloseEvent* event) override; + +private: + void appendFormattedLine(const QString& line, ActivityLevel level, bool is_error); + void queueEvent(ActivityEvent event); + void flushPendingEvents(); + bool shouldShow(ActivityLevel level) const; + void loadRecentHistory(); + void updateStatusBar(); + + static DevHelperWindow* s_instance; + + QPlainTextEdit* m_logView = nullptr; + QLabel* m_statusLabel = nullptr; + QCheckBox* m_errorsOnlyCheck = nullptr; + QCheckBox* m_alwaysOnTopCheck = nullptr; + QCheckBox* m_raiseOnErrorCheck = nullptr; + QPushButton* m_clearButton = nullptr; + QPushButton* m_copyAllButton = nullptr; + QPushButton* m_copyLastButton = nullptr; + QPushButton* m_copyIncidentButton = nullptr; + QPushButton* m_selectAllButton = nullptr; + QPushButton* m_openLogButton = nullptr; + boost::signals2::scoped_connection m_event_connection; + bool m_pinnedForSession = false; + bool m_flushScheduled = false; + std::mutex m_pendingMutex; + int m_error_count = 0; + int m_visible_count = 0; + QStringList m_allLines; + std::vector m_pendingEvents; + static constexpr int kMaxStoredLines = 50000; + static constexpr int kCopyLastLines = 200; +}; + +#endif // ENABLE_DEV_HELPER_WINDOW + +#endif // BITCOIN_QT_DEVHELPERWINDOW_H diff --git a/src/qt/devtools.cpp b/src/qt/devtools.cpp new file mode 100644 index 0000000000..c5e7f6098f --- /dev/null +++ b/src/qt/devtools.cpp @@ -0,0 +1,124 @@ +// Copyright (c) 2026 The Vericoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#if ENABLE_DEV_HELPER_WINDOW + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +bool PromptUnlock(QWidget* parent) +{ + if (!IsDeveloperEditionActive()) + return false; + + if (IsDevToolsUnlocked()) + return true; + + QString prompt = QObject::tr("Master password:"); + + for (;;) { + bool ok = false; + const QString password = QInputDialog::getText( + parent, + QObject::tr("Unlock Developer Tools"), + prompt, + QLineEdit::Password, + QString(), + &ok); + if (!ok || password.isEmpty()) + return false; + + if (UnlockDevToolsWithPassword(password.toStdString())) + return true; + + const int choice = QMessageBox::warning( + parent, + QObject::tr("Developer Edition"), + QObject::tr("Incorrect master password."), + QMessageBox::Retry | QMessageBox::Cancel, + QMessageBox::Retry); + if (choice != QMessageBox::Retry) + return false; + + prompt = QObject::tr("Master password (try again):"); + } +} + +void ShowTraceWindow(QWidget* parent) +{ + InitActivityLog(); + LogActivityEx(ActivityLevel::Info, __FILE__, __LINE__, __func__, + "Verbose trace session starting (before chain init and bootstrap prompts)"); + + // Defer window creation so modal password dialogs fully dismiss and init + // is not blocked. debug.log mirroring is installed later in init.cpp after + // StartLogging() to avoid flooding the GUI with buffered log lines. + QTimer::singleShot(0, parent, [parent]() { + DevHelperWindow* window = DevHelperWindow::CreateAndShow(parent); + if (window) + window->setPinnedForSession(true); + }); +} + +} // namespace + +namespace DevTools { + +void OfferStartupTraceWindow(QWidget* parent) +{ + if (!IsDeveloperEditionActive()) { + MarkDevStartupPromptComplete(); + return; + } + + if (DevHelperWindow::instance() && DevHelperWindow::instance()->isPinnedForSession()) { + MarkDevStartupPromptComplete(); + return; + } + + const int ret = QMessageBox::question( + parent, + QObject::tr("Developer Edition"), + QObject::tr("Open the Verbose Dev Trace window for this session?\n\n" + "Choose now, before chain loading or bootstrap — so if anything freezes " + "you can see exactly where it stopped.\n\n" + "The trace window stays open for the whole session. " + "If you choose No, dev tools are unavailable until restart."), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes); + + if (ret != QMessageBox::Yes) { + LogActivityEx(ActivityLevel::Info, __FILE__, __LINE__, __func__, + "User declined startup verbose trace (no dev UI this session; logging to activity.log still begins at init)"); + MarkDevStartupPromptComplete(); + return; + } + + if (!PromptUnlock(parent)) { + LogActivityEx(ActivityLevel::Warning, __FILE__, __LINE__, __func__, + "Startup verbose trace cancelled at password prompt"); + MarkDevStartupPromptComplete(); + return; + } + + ShowTraceWindow(parent); + QCoreApplication::processEvents(); + MarkDevStartupPromptComplete(); +} + +} // namespace DevTools + +#endif // ENABLE_DEV_HELPER_WINDOW diff --git a/src/qt/devtools.h b/src/qt/devtools.h new file mode 100644 index 0000000000..72b6d7ce53 --- /dev/null +++ b/src/qt/devtools.h @@ -0,0 +1,23 @@ +// Copyright (c) 2026 The Vericoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_DEVTOOLS_H +#define BITCOIN_QT_DEVTOOLS_H + +#include + +#if ENABLE_DEV_HELPER_WINDOW + +class QWidget; + +namespace DevTools { + +/** One-time startup prompt: open verbose trace or no dev access until restart. */ +void OfferStartupTraceWindow(QWidget* parent); + +} // namespace DevTools + +#endif // ENABLE_DEV_HELPER_WINDOW + +#endif // BITCOIN_QT_DEVTOOLS_H diff --git a/src/qt/forms/bootstrapdialog.ui b/src/qt/forms/bootstrapdialog.ui index f6bacbca2a..affed8bd9f 100644 --- a/src/qt/forms/bootstrapdialog.ui +++ b/src/qt/forms/bootstrapdialog.ui @@ -6,84 +6,475 @@ 0 0 - 466 - 196 + 560 + 420 + + + 520 + 0 + + - Dialog + Bootstrap Blockchain - - - - 33 - 20 - 411 - 51 - - - - The client will bootstrap the chain. Please be patient. - - - - - - 33 - 70 - 411 - 41 - - - - 0 - - - true - - - false - - - - - - 353 - 140 - 88 - 34 - + + + QLayout::SetMinimumSize - - Start + + 24 - - - - - 250 - 140 - 88 - 34 - + + 20 - - Close + + 24 - - - - - 30 - 140 - 311 - 41 - + + 20 - - install new configuration file verium.conf + + 14 - + + + + Sync paused for bootstrap + + + Qt::AlignCenter + + + false + + + + + + + 0 + + + + + + + Speed up blockchain sync + + + true + + + + + + + Download a recent blockchain snapshot from Vericonomy servers instead of syncing the entire chain from the network. + +Network sync will pause during download and extract. Verium will shut down when the archive is ready and must be restarted manually. A wallet rescan may run on the next launch. + + + true + + + + + + + + + + true + + + false + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Sync from network + + + + + + + Download bootstrap + + + + + + + + + + + + + Downloading bootstrap archive + + + true + + + + + + + Downloading from files.vericonomy.com over HTTPS. + + + true + + + + + + + + + 1 Download + + + + + + + + + + + + + + 2 Extract + + + + + + + + + + + + + + 3 Validate + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 0% + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 0 + + + false + + + + + + + Show details + + + true + + + + + + + + 16777215 + 80 + + + + true + + + false + + + + + + + Qt::Vertical + + + + 20 + 10 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + + + + + + + Download interrupted + + + true + + + + + + + The wallet kept the partial archive and can resume from the last byte. + + + true + + + + + + + QFrame::StyledPanel + + + + + + Connection lost while downloading + + + true + + + + + + + + + + true + + + Qt::TextSelectableByMouse + + + + + + + + + + true + + + + + + + + + + 0 + + + false + + + + + + + Tip: Retry uses the same destination and resumes from the saved partial download. + + + true + + + + + + + + 16777215 + 80 + + + + true + + + false + + + + + + + Qt::Vertical + + + + 20 + 10 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + Details + + + + + + + Retry + + + + + + + + + + diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 3c0d70201b..e843f66c6f 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -19,6 +19,7 @@ #include