From ea675689c560783cba5076a23bb7a3c321283201 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 9 Jun 2025 21:20:46 +0200 Subject: [PATCH 1/9] jam remote update to v0.4.0 (#175) --- scripts/jam-remote/install.jam.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/jam-remote/install.jam.sh b/scripts/jam-remote/install.jam.sh index 8b015f1..9d5af12 100644 --- a/scripts/jam-remote/install.jam.sh +++ b/scripts/jam-remote/install.jam.sh @@ -3,7 +3,7 @@ # https://github.com/joinmarket-webui/jam USERNAME=jam -WEBUI_VERSION="v0.3.0" +WEBUI_VERSION="v0.4.0" REPO=joinmarket-webui/jam HOME_DIR=/home/${USERNAME} APP_DIR=webui From f92be4e5b3e963056141fafee239979a207fa71e Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 9 Jun 2025 21:51:39 +0200 Subject: [PATCH 2/9] docs: remove inactive donation links from readme (#176) --- README.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3aa7a3b..a9f254f 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ A minimalistic, security focused linux environment for JoinMarket with a termina - [Features](#features) - [Required Hardware](#required-hardware) - [A computer running a Debian / Ubuntu Linux flavour.](#a-computer-running-a-debian--ubuntu-linux-flavour) - - [RaspberryPi 4 or 3](#raspberrypi-4-or-3) + - [RaspberryPi 5 or 4](#raspberrypi-5-or-4) - [VPS eg: host4coins.net](#vps-eg-host4coinsnet) - [Set up using an SDcard image](#set-up-using-an-sdcard-image) - [Set up JoininBox on Linux](#set-up-joininbox-on-linux) @@ -32,7 +32,6 @@ A minimalistic, security focused linux environment for JoinMarket with a termina - [More info](#more-info) - [About JoinMarket](#about-joinmarket) - [Forums](#forums) -- [Donations](#donations) ## Features @@ -44,7 +43,7 @@ A minimalistic, security focused linux environment for JoinMarket with a termina * RaspiBlitz over [LAN or Tor](prepare_remote_node.md#raspiblitz) * RoninDojo over [LAN or Tor](prepare_remote_node.md#ronindojo) * Start a pruned node from https://pruned.host4coins.net/blocks -* JoininBox is part the RaspiBlitz SERVICES +* JoininBox is part of the RaspiBlitz SERVICES **The addresses, transactions and balances of JoinMarket can be seen in the watch-only wallet of the connected node.** * use your own or a trusted node @@ -53,10 +52,10 @@ A minimalistic, security focused linux environment for JoinMarket with a termina ## Required Hardware ### A computer running a Debian / Ubuntu Linux flavour. * See the [tested-environments](#tested-environments). -### RaspberryPi 4 or 3 +### RaspberryPi 5 or 4 * Power supply (5V 3A and above recommended) * Heatsink case -* 32 GB Endurence type SDcard +* 32 GB Endurance type SDcard * [(USB SSD to run a pruned bitcoin node locally)](FAQ.md#usb-ssd-recommendation) ### VPS eg: [host4coins.net](https://host4coins.net/) Recommended minimum: @@ -162,8 +161,3 @@ the default password is: `joininbox` - will be prompted to change it on the firs * IRC: #joinmarket on [libera.chat](https://libera.chat/) or [hackint.org](https://hackint.org/) * Reddit: * Keybase: - -## Donations -* For JoinMarket (general): https://bitcoinprivacy.me/joinmarket-donations -* To waxwing for JoinMarket: -* To openoms for JoininBox (LN + payjoin enabled - open in the [Tor Browser](https://www.torproject.org/)): From 7a61e3b357c7a62d7eeb733e1d85bacce09e3405 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 18 Jan 2026 07:56:26 +0100 Subject: [PATCH 3/9] update JoinMarket to commit ce32baf, support for Python 3.12 / 3.13, modernize build (#178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates JoinMarket to the latest tested commit with Python 3.12/3.13 support and addresses dialog display issues. ### Changes #### JoinMarket Update - Update to commit `ce32baf` (latest tested commit from master) - Refactored install.joinmarket.sh to support both tag-based and commit-based installations - Improved signature verification using verify.git.sh - Updated menu.update.sh to display commit hash when using commit-based version #### Python Support - Added support for Python 3.12 and 3.13 - Changed version detection order to prefer newer Python versions (3.13 → 3.12 → 3.11 → 3.10 → 3.9 → 3.8) #### Dialog Fixes - **Fix border visibility**: Changed dialog border color from `(BLACK,BLACK,ON)` to `(CYAN,BLACK,ON)` in .dialogrc for better visibility - **Terminal compatibility**: Added fallback for unknown terminal types (e.g., `xterm-ghostty`) by setting `TERM=xterm-256color` when `infocmp` fails - **Fix box drawing characters**: Set `NCURSES_NO_UTF8_ACS=1` to fix dialog box rendering issues #### Documentation - Fixed typo in prepare_remote_node.md: "numbes" → "numbers" --- .editorconfig | 15 +++ build_joininbox.sh | 36 +++--- ci/amd64/debian/build.amd64-debian.pkr.hcl | 115 ++++++++++++++++++++ ci/amd64/debian/http/debian-9/preseed.cfg | 47 -------- ci/amd64/debian/http/preseed.cfg | 72 ++++++++++++ ci/amd64/debian/joininbox-amd64-debian.json | 70 ------------ ci/amd64/packer.build.amd64-debian.sh | 39 +++---- prepare_remote_node.md | 2 +- scripts/.dialogrc | 2 +- scripts/_functions.sh | 8 ++ scripts/install.joinmarket.sh | 69 ++++++------ scripts/menu.update.sh | 8 +- 12 files changed, 296 insertions(+), 187 deletions(-) create mode 100644 .editorconfig create mode 100644 ci/amd64/debian/build.amd64-debian.pkr.hcl delete mode 100644 ci/amd64/debian/http/debian-9/preseed.cfg create mode 100644 ci/amd64/debian/http/preseed.cfg delete mode 100644 ci/amd64/debian/joininbox-amd64-debian.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..07f5c90 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig helps maintain consistent coding styles +# https://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[Makefile] +indent_style = tab diff --git a/build_joininbox.sh b/build_joininbox.sh index e8074c5..ade914a 100644 --- a/build_joininbox.sh +++ b/build_joininbox.sh @@ -276,22 +276,30 @@ echo "##########" echo # apt dependencies for python apt-get install -y python3 virtualenv python3-venv python3-dev python3-wheel python3-jinja2 python3-pip -if [ -f "/usr/bin/python3.8" ]; then - # use python 3.8 if available - update-alternatives --install /usr/bin/python python /usr/bin/python3.8 1 - echo "# python calls python3.8" -elif [ -f "/usr/bin/python3.9" ]; then - # use python 3.9 if available - update-alternatives --install /usr/bin/python python /usr/bin/python3.9 1 - echo "# python calls python3.9" -elif [ -f "/usr/bin/python3.10" ]; then - # use python 3.10 if available - update-alternatives --install /usr/bin/python python /usr/bin/python3.10 1 - echo "# python calls python3.10" +if [ -f "/usr/bin/python3.13" ]; then + # use python 3.13 if available + update-alternatives --install /usr/bin/python python /usr/bin/python3.13 1 + echo "# python calls python3.13" +elif [ -f "/usr/bin/python3.12" ]; then + # use python 3.12 if available + update-alternatives --install /usr/bin/python python /usr/bin/python3.12 1 + echo "# python calls python3.12" elif [ -f "/usr/bin/python3.11" ]; then # use python 3.11 if available update-alternatives --install /usr/bin/python python /usr/bin/python3.11 1 echo "# python calls python3.11" +elif [ -f "/usr/bin/python3.10" ]; then + # use python 3.10 if available + update-alternatives --install /usr/bin/python python /usr/bin/python3.10 1 + echo "# python calls python3.10" +elif [ -f "/usr/bin/python3.9" ]; then + # use python 3.9 if available + update-alternatives --install /usr/bin/python python /usr/bin/python3.9 1 + echo "# python calls python3.9" +elif [ -f "/usr/bin/python3.8" ]; then + # use python 3.8 if available + update-alternatives --install /usr/bin/python python /usr/bin/python3.8 1 + echo "# python calls python3.8" else echo "# FAIL- there is no tested version of python present" exit 1 @@ -391,11 +399,11 @@ else exit 1 fi -command="sudo -u joinmarket bash /home/joinmarket/joininbox/scripts/verify.git.sh \ +command="bash /home/joinmarket/joininbox/scripts/verify.git.sh \ ${PGPsigner} ${PGPpubkeyLink} ${PGPpubkeyFingerprint}" echo "running: ${command}" chmod 777 /dev/shm -${command} || exit 1 +sudo -u joinmarket ${command} || exit 1 runuser joinmarket -c "cp /home/joinmarket/joininbox/scripts/* /home/joinmarket/" runuser joinmarket -c "cp /home/joinmarket/joininbox/scripts/.* /home/joinmarket/ 2>/dev/null" diff --git a/ci/amd64/debian/build.amd64-debian.pkr.hcl b/ci/amd64/debian/build.amd64-debian.pkr.hcl new file mode 100644 index 0000000..d7892e4 --- /dev/null +++ b/ci/amd64/debian/build.amd64-debian.pkr.hcl @@ -0,0 +1,115 @@ +# images, checksums and signatures are at: +# https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/ +variable "iso_name" { default = "debian-13.3.0-amd64-netinst.iso" } +variable "iso_checksum" { default = "c9f09d24b7e834e6834f2ffa565b33d6f1f540d04bd25c79ad9953bc79a8ac02" } + +variable "github_user" { default = "openoms" } +variable "branch" { default = "master" } + +variable "boot" { default = "uefi" } +variable "preseed_file" { default = "preseed.cfg" } +variable "hostname" { default = "joininbox-amd64" } + +variable "image_size" { default = "30000" } +variable "image_type" { default = "qcow2" } + +variable "memory" { default = "2048" } +variable "cpus" { default = "2" } + +locals { + name_template = "joininbox-amd64-debian" + image_extension = var.image_type == "raw" ? "img" : var.image_type + bios_file = var.boot == "uefi" ? "OVMF.fd" : "bios-256k.bin" + boot_command = var.boot == "uefi" ? [ + "c", + "linux /install.amd/vmlinuz ", + "auto=true ", + "url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/${var.preseed_file} ", + "hostname=${var.hostname} ", + "domain=${var.hostname}.local ", + "interface=auto ", + "vga=788 noprompt quiet --", + "initrd /install.amd/initrd.gz", + "boot" + ] : [ + "install ", + " preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/${var.preseed_file} ", + "debian-installer=en_US.UTF-8 ", + "auto ", + "locale=en_US.UTF-8 ", + "kbd-chooser/method=us ", + "keyboard-configuration/xkb-keymap=us ", + "netcfg/get_hostname=${var.hostname} ", + "netcfg/get_domain=${var.hostname}.local ", + "fb=false ", + "debconf/frontend=noninteractive ", + "console-setup/ask_detect=false ", + "console-keymaps-at/keymap=us ", + "grub-installer/bootdev=default ", + "" + ] +} + +source "qemu" "debian" { + boot_command = local.boot_command + boot_wait = "5s" + cpus = var.cpus + disk_size = var.image_size + http_directory = "./http" + iso_checksum = var.iso_checksum + iso_url = "https://cdimage.debian.org/cdimage/release/current/amd64/iso-cd/${var.iso_name}" + memory = var.memory + output_directory = "../builds/${local.name_template}-qemu" + shutdown_command = "echo 'joininbox' | sudo /sbin/shutdown -hP now" + ssh_password = "joininbox" + ssh_port = 22 + ssh_timeout = "10000s" + ssh_username = "joinmarket" + format = var.image_type + vm_name = "${local.name_template}.${local.image_extension}" + headless = false + vnc_bind_address = "127.0.0.1" + vnc_port_max = 5900 + vnc_port_min = 5900 + qemuargs = [ + ["-m", var.memory], + ["-bios", local.bios_file], + ["-display", "none"] + ] +} + +build { + description = "JoininBox amd64 Debian image build" + sources = ["source.qemu.debian"] + + provisioner "shell" { + environment_vars = [ + "HOME_DIR=/home/joinmarket", + "github_user=${var.github_user}", + "branch=${var.branch}" + ] + + execute_command = "echo 'joininbox' | {{.Vars}} sudo -S -E sh -eux '{{.Path}}'" + expect_disconnect = true + scripts = [ + "./scripts/update.sh", + "./../_common/sshd.sh", + "./scripts/networking.sh", + "./scripts/sudoers.sh", + "./scripts/systemd.sh", + "./scripts/joininbox.sh", + "./scripts/cleanup.sh" + ] + } +} + +packer { + required_version = ">= 1.7.0, < 2.0.0" + + required_plugins { + qemu = { + source = "github.com/hashicorp/qemu" + version = ">= 1.0.0, < 2.0.0" + } + } +} diff --git a/ci/amd64/debian/http/debian-9/preseed.cfg b/ci/amd64/debian/http/debian-9/preseed.cfg deleted file mode 100644 index 4bc9d0d..0000000 --- a/ci/amd64/debian/http/debian-9/preseed.cfg +++ /dev/null @@ -1,47 +0,0 @@ -choose-mirror-bin mirror/http/proxy string -d-i apt-setup/use_mirror boolean true -d-i base-installer/kernel/override-image string linux-server -d-i clock-setup/utc boolean true -d-i clock-setup/utc-auto boolean true -d-i finish-install/reboot_in_progress note -d-i grub-installer/only_debian boolean true -d-i grub-installer/with_other_os boolean true -d-i keymap select us -d-i mirror/country string manual -d-i mirror/http/directory string /debian -d-i mirror/http/hostname string ftp.de.debian.org -d-i mirror/http/proxy string -d-i partman-auto-lvm/guided_size string max -d-i partman-auto/choose_recipe select atomic -d-i partman-auto/method string lvm -d-i partman-lvm/confirm boolean true -d-i partman-lvm/confirm_nooverwrite boolean true -d-i partman-lvm/device_remove_lvm boolean true -d-i partman/choose_partition select finish -d-i partman/confirm boolean true -d-i partman/confirm_nooverwrite boolean true -d-i partman/confirm_write_new_label boolean true -d-i passwd/root-login boolean false -d-i passwd/root-password-again password joininbox -d-i passwd/root-password password joininbox -d-i passwd/user-fullname string joinmarket -d-i passwd/user-uid string 1000 -d-i passwd/user-password password joininbox -d-i passwd/user-password-again password joininbox -d-i passwd/username string joinmarket -d-i pkgsel/include string sudo bzip2 acpid cryptsetup zlib1g-dev wget curl dkms fuse make nfs-common net-tools cifs-utils rsync -d-i pkgsel/install-language-support boolean false -d-i pkgsel/update-policy select none -d-i pkgsel/upgrade select full-upgrade -# Prevent packaged version of VirtualBox Guest Additions being installed: -d-i preseed/early_command string sed -i \ - '/in-target/idiscover(){/sbin/discover|grep -v VirtualBox;}' \ - /usr/lib/pre-pkgsel.d/20install-hwpackages -d-i time/zone string UTC -d-i user-setup/allow-password-weak boolean true -d-i user-setup/encrypt-home boolean false -d-i preseed/late_command string sed -i '/^deb cdrom:/s/^/#/' /target/etc/apt/sources.list -apt-cdrom-setup apt-setup/cdrom/set-first boolean false -apt-mirror-setup apt-setup/use_mirror boolean true -popularity-contest popularity-contest/participate boolean false -tasksel tasksel/first multiselect standard, ssh-server diff --git a/ci/amd64/debian/http/preseed.cfg b/ci/amd64/debian/http/preseed.cfg new file mode 100644 index 0000000..244ea2b --- /dev/null +++ b/ci/amd64/debian/http/preseed.cfg @@ -0,0 +1,72 @@ +# https://github.com/chef/bento/blob/main/packer_templates/http/debian/preseed.cfg +# https://www.debian.org/releases/stable/example-preseed.txt +# https://github.com/tylert/packer-build/blob/master/source/debian/12_bookworm/base-uefi.preseed +# variables: https://github.com/tylert/packer-build/blob/master/source/debian/12_bookworm/base-uefi.pkr.hcl + +# Locale Setup +d-i debian-installer/language string en +d-i debian-installer/country string US +d-i debian-installer/locale string en_US.UTF-8 +# d-i localechooser/supported-locales multiselect en_CA.UTF-8 fr_CA.UTF-8 zh_CN.UTF-8 +# d-i pkgsel/install-language-support boolean true + +# Keyboard Setup +d-i keyboard-configuration/xkb-keymap select us + +# Clock Setup +# d-i time/zone string Canada/Eastern +d-i time/zone string UTC +d-i clock-setup/utc boolean true +# set above to false if making a bootable USB to run on same system as Windows + +# Network Setup +d-i netcfg/get_hostname string joininbox-amd64 +d-i netcfg/get_domain string +# https://bugs.launchpad.net/ubuntu/+source/netcfg/+bug/713385 +d-i netcfg/choose_interface select auto +# make sure you also add "interface=auto" to your boot command too +# https://bugs.launchpad.net/ubuntu/+source/netcfg/+bug/713385 + +# User Setup +d-i passwd/root-login boolean false +d-i passwd/root-password-again password joininbox +d-i passwd/root-password password joininbox +d-i passwd/user-fullname string joinmarket +d-i passwd/user-uid string 1000 +d-i passwd/user-password password joininbox +d-i passwd/user-password-again password joininbox +d-i passwd/username string joinmarket + +# Package Setup +d-i hw-detect/load_firmware boolean false +d-i hw-detect/load_media boolean false +apt-cdrom-setup apt-setup/cdrom/set-first boolean false +d-i mirror/country string manual +d-i mirror/http/hostname string httpredir.debian.org +d-i mirror/http/directory string /debian +d-i mirror/http/proxy string +d-i apt-setup/contrib boolean true +d-i apt-setup/non-free boolean true + +tasksel tasksel/first multiselect ssh-server, standard +d-i pkgsel/include string sudo bzip2 acpid cryptsetup zlib1g-dev wget curl dkms fuse make nfs-common net-tools cifs-utils rsync +d-i pkgsel/install-language-support boolean false +d-i pkgsel/update-policy select none +d-i pkgsel/upgrade select full-upgrade + +popularity-contest popularity-contest/participate boolean false + +# Drive setup +d-i partman-auto-lvm/guided_size string max +d-i partman-auto/choose_recipe select atomic +d-i partman-auto/method string lvm +d-i partman-lvm/confirm boolean true +d-i partman-lvm/confirm_nooverwrite boolean true +d-i partman-lvm/device_remove_lvm boolean true +d-i partman/choose_partition select finish +d-i partman/confirm boolean true +d-i partman/confirm_nooverwrite boolean true +d-i partman/confirm_write_new_label boolean true + +# Final Setup +d-i finish-install/reboot_in_progress note diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json deleted file mode 100644 index 7e1bef0..0000000 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "variables": { - "iso_name": "debian-12.11.0-amd64-netinst.iso", - "iso_checksum": "30ca12a15cae6a1033e03ad59eb7f66a6d5a258dcf27acd115c2bd42d22640e8", - "box_basename": "debian", - "build_directory": "../builds", - "headless": "false", - "http_directory": "{{template_dir}}/http", - "name": "debian", - "preseed_path": "debian-9/preseed.cfg", - "qemu_bios": "bios-256k.bin", - "template": "joininbox-amd64-debian", - "boot_command": "install preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/{{user `preseed_path`}} debian-installer=en_US.UTF-8 auto locale=en_US.UTF-8 kbd-chooser/method=us keyboard-configuration/xkb-keymap=us netcfg/get_hostname={{ .Name }} netcfg/get_domain=vagrantup.com fb=false debconf/frontend=noninteractive console-setup/ask_detect=false console-keymaps-at/keymap=us grub-installer/bootdev=default ", - "memory": "2048" - }, - "builders": [ - { - "boot_command": "{{user `boot_command`}}", - "boot_wait": "5s", - "cpus": "2", - "disk_size": "30000", - "headless": "{{ user `headless` }}", - "http_directory": "{{user `http_directory`}}", - "iso_checksum": "{{user `iso_checksum`}}", - "iso_url": "http://cdimage.debian.org/cdimage/release/current/amd64/iso-cd/{{user `iso_name`}}", - "memory": "{{ user `memory` }}", - "output_directory": "{{ user `build_directory` }}/{{user `template`}}-qemu", - "shutdown_command": "echo 'joininbox' | sudo /sbin/shutdown -hP now", - "ssh_password": "joininbox", - "ssh_port": "22", - "ssh_timeout": "10000s", - "ssh_username": "joinmarket", - "type": "qemu", - "format": "qcow2", - "vm_name": "{{ user `template` }}.qcow2", - "vnc_bind_address": "127.0.0.1", - "vnc_port_max": "5900", - "vnc_port_min": "5900", - "qemuargs": [ - [ "-m", "{{ user `memory` }}" ], - [ "-bios", "{{ user `qemu_bios` }}" ], - [ "-display", "none" ] - ] - } - ], - "provisioners": [ - { - "type": "shell", - "environment_vars": [ - "HOME_DIR=/home/joinmarket", - "http_proxy={{user `http_proxy`}}", - "https_proxy={{user `https_proxy`}}", - "no_proxy={{user `no_proxy`}}", - "github_user={{user `github_user`}}", - "branch={{user `branch`}}" - ], - "execute_command": "echo 'joininbox' | {{.Vars}} sudo -S -E sh -eux '{{.Path}}'", - "expect_disconnect": true, - "scripts": [ - "{{template_dir}}/scripts/update.sh", - "{{template_dir}}/../_common/sshd.sh", - "{{template_dir}}/scripts/networking.sh", - "{{template_dir}}/scripts/sudoers.sh", - "{{template_dir}}/scripts/systemd.sh", - "{{template_dir}}/scripts/joininbox.sh", - "{{template_dir}}/scripts/cleanup.sh" - ] - } - ] -} diff --git a/ci/amd64/packer.build.amd64-debian.sh b/ci/amd64/packer.build.amd64-debian.sh index 1970b41..bf7a0f4 100644 --- a/ci/amd64/packer.build.amd64-debian.sh +++ b/ci/amd64/packer.build.amd64-debian.sh @@ -1,39 +1,40 @@ #!/bin/bash -e +sudo apt-get update + # install packer if ! packer version 2>/dev/null; then - curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - - sudo apt-add-repository -y "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" - sudo apt-get update - echo -e "\nInstalling packer..." - sudo apt-get install -y packer + curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - + sudo apt-add-repository -y "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" + sudo apt-get update + echo -e "\nInstalling packer..." + sudo apt-get install -y packer else - echo "# Packer is installed" + echo "# Packer is installed" fi -# install qemu +# install qemu and UEFI firmware echo "# Install qemu ..." sudo apt-get update -sudo apt-get install -y qemu-system - -# install qemu plugin -packer plugins install github.com/hashicorp/qemu +sudo apt-get install -y qemu-system ovmf +# set vars from positional arguments (for backward compatibility with CI) if [ $# -gt 0 ]; then - github_user=$1 + github_user=$1 else - github_user=openoms + github_user=openoms fi if [ $# -gt 1 ]; then - branch=$2 + branch=$2 else - branch=master + branch=master fi +vars="-var github_user=${github_user} -var branch=${branch}" + # Build the image -echo "# Building image ..." +echo "# Build the image with: github_user=${github_user} branch=${branch}" cd debian -PACKER_LOG=1 packer build \ - -var github_user=${github_user} -var branch=${branch} \ - -only=qemu joininbox-amd64-debian.json +packer init -upgrade . +PACKER_LOG=1 packer build ${vars} -only=qemu.debian build.amd64-debian.pkr.hcl || exit 1 diff --git a/prepare_remote_node.md b/prepare_remote_node.md index 628bc24..f5c78bd 100644 --- a/prepare_remote_node.md +++ b/prepare_remote_node.md @@ -45,7 +45,7 @@ This can be skipped if you [connect through Tor](#tor-connection) Add the values: * `rpcallowip=JOININBOX_IP` or `RANGE` * either specify the LAN IP of the computer (here JoininBox) - * or use a range like: `192.168.1.0/24` - edit to your local subnet - the first 3 numbes of the LAN IP address, the example used here is: 192.168.1.x + * or use a range like: `192.168.1.0/24` - edit to your local subnet - the first 3 numbers of the LAN IP address, the example used here is: 192.168.1.x * `rpcbind=LAN_IP_OF_THE_NODE` * use the local IP of the bitcoin node in the example: `192.168.1.4` * can keep the other `rpcallowip` and `rpcbind` entries especially for the localhost: `127.0.0.1` diff --git a/scripts/.dialogrc b/scripts/.dialogrc index 23b648f..e60c81a 100644 --- a/scripts/.dialogrc +++ b/scripts/.dialogrc @@ -42,7 +42,7 @@ dialog_color = (CYAN,BLACK,OFF) title_color = (CYAN,BLACK,ON) # Dialog box border color -border_color = (BLACK,BLACK,ON) +border_color = (CYAN,BLACK,ON) # Active button color button_active_color = (BLACK,CYAN,ON) diff --git a/scripts/_functions.sh b/scripts/_functions.sh index 4c69bce..ea5777c 100755 --- a/scripts/_functions.sh +++ b/scripts/_functions.sh @@ -14,6 +14,14 @@ walletPath="/home/joinmarket/.joinmarket/wallets/" JMcfgPath="/home/joinmarket/.joinmarket/joinmarket.cfg" joininConfPath="/home/joinmarket/joinin.conf" +## dialog fixes +# fallback for unknown terminal types (e.g., xterm-ghostty) +if ! infocmp "$TERM" &>/dev/null; then + export TERM=xterm-256color +fi +# fix dialog box drawing characters +export NCURSES_NO_UTF8_ACS=1 + # functions source /home/joinmarket/_functions.menu.sh source /home/joinmarket/_functions.bitcoincore.sh diff --git a/scripts/install.joinmarket.sh b/scripts/install.joinmarket.sh index 928848e..6be93d8 100755 --- a/scripts/install.joinmarket.sh +++ b/scripts/install.joinmarket.sh @@ -1,7 +1,12 @@ #!/bin/bash # https://github.com/JoinMarket-Org/joinmarket-clientserver/releases -testedJMversion="v0.9.11" +# Use tag for verification when available, otherwise use commit hash +# testedJMversion="v0.9.11" + +# https://github.com/JoinMarket-Org/joinmarket-clientserver/commits/master/ +# Only used if testedJMversion is empty or not set +testedJMcommit="ce32bafbb5d716bde61830f71266410249d43dbc" PGPsigner="kristapsk" PGPpkeys="https://github.com/kristapsk.gpg" @@ -21,7 +26,7 @@ usage() { printf %s"${me} [--option ] a script to install, update or configure JoinMarket -the latest tested version: $testedJMversion is installed by default with the QT GUI +the latest tested version: ${testedJMversion:-$testedJMcommit} is installed by default with the QT GUI Options: -h, --help this help info @@ -110,8 +115,12 @@ done : "${install:=install}" range_argument install "install" "config" "update" "testPR" "commit" -: "${version:=${testedJMversion}}" -curl -s "https://github.com/JoinMarket-Org/joinmarket-clientserver/release/tag/${version}" | grep -q "\"message\": \"Version not found\"" && error_msg "'There is no: https://github.com/JoinMarket-Org/joinmarket-clientserver/release/tag/${version}'" +# Use tag if set, otherwise use commit hash +: "${version:=${testedJMversion:-$testedJMcommit}}" +# Only check GitHub releases if version looks like a tag (starts with 'v') +if [[ "${version}" == v* ]]; then + curl -s "https://github.com/JoinMarket-Org/joinmarket-clientserver/release/tag/${version}" | grep -q "\"message\": \"Version not found\"" && error_msg "'There is no: https://github.com/JoinMarket-Org/joinmarket-clientserver/release/tag/${version}'" +fi : "${qtgui:=false}" range_argument qtgui "0" "1" "false" "true" @@ -183,37 +192,29 @@ function installJoinMarket() { elif [ "$install" = "update" ] && [ ${#2} -gt 0 ]; then updateVersion="$2" sudo -u ${user} git reset --hard $updateVersion - else - sudo -u ${user} git reset --hard $testedJMversion - - sudo -u ${user} wget --prefer-family=ipv4 -O "pgp_keys.asc" ${PGPpkeys} - sudo -u ${user} gpg --import --import-options show-only ./pgp_keys.asc - fingerprint=$(sudo -u ${user} gpg "pgp_keys.asc" 2>/dev/null | grep "${PGPcheck}" -c) - if [ ${fingerprint} -lt 1 ]; then - echo - echo "# WARNING --> the PGP fingerprint is not as expected for ${PGPsigner}" - echo "# Should contain PGP: ${PGPcheck}" - echo "# PRESS ENTER to TAKE THE RISK if you think all is OK" - read -r - fi - sudo -u ${user} gpg --import ./pgp_keys.asc - - verifyResult=$(sudo -u ${user} git verify-tag $testedJMversion 2>&1) - - goodSignature=$(echo ${verifyResult} | grep 'Good signature' -c) - echo "# goodSignature(${goodSignature})" - correctKey=$(echo ${verifyResult} | tr -d " \t\n\r" | grep "${PGPcheck}" -c) - echo "# correctKey(${correctKey})" - if [ ${correctKey} -lt 1 ] || [ ${goodSignature} -lt 1 ]; then - echo - echo "# BUILD FAILED --> PGP verification not OK / signature(${goodSignature}) verify(${correctKey})" - exit 1 + echo "# Verifying signature for version: $updateVersion" + # Determine if it's a tag (starts with 'v') or commit hash + if [[ "$updateVersion" == v* ]]; then + sudo -u ${user} bash /home/joinmarket/joininbox/scripts/verify.git.sh \ + "${PGPsigner}" "${PGPpkeys}" "${PGPcheck}" "$updateVersion" || exit 1 else - echo - echo "#########################################################" - echo "# OK --> the PGP signature of the $testedJMversion tag is correct" - echo "#########################################################" - echo + sudo -u ${user} bash /home/joinmarket/joininbox/scripts/verify.git.sh \ + "${PGPsigner}" "${PGPpkeys}" "${PGPcheck}" || exit 1 + fi + else + # Decide whether to use tag or commit + if [ -n "${testedJMversion}" ]; then + echo "# Installing tested version: ${testedJMversion}" + sudo -u ${user} git reset --hard $testedJMversion + echo "# Verifying tag signature: ${testedJMversion}" + sudo -u ${user} bash /home/joinmarket/joininbox/scripts/verify.git.sh \ + "${PGPsigner}" "${PGPpkeys}" "${PGPcheck}" "${testedJMversion}" || exit 1 + else + echo "# Installing tested commit: ${testedJMcommit}" + sudo -u ${user} git reset --hard $testedJMcommit + echo "# Verifying commit signature: ${testedJMcommit}" + sudo -u ${user} bash /home/joinmarket/joininbox/scripts/verify.git.sh \ + "${PGPsigner}" "${PGPpkeys}" "${PGPcheck}" || exit 1 fi fi diff --git a/scripts/menu.update.sh b/scripts/menu.update.sh index e854304..c1321bf 100755 --- a/scripts/menu.update.sh +++ b/scripts/menu.update.sh @@ -16,9 +16,15 @@ OPTIONS=() BACKTITLE="JoininBox GUI" # Basic Options +# Determine if using tag or commit for display +testedVersion=$(grep 'testedJMversion=' < ~/install.joinmarket.sh | grep -v '^#' | cut -d '"' -f 2) +if [ -z "$testedVersion" ]; then + testedVersion=$(grep 'testedJMcommit=' < ~/install.joinmarket.sh | cut -d '"' -f 2 | cut -c 1-12) + testedVersion="${testedVersion} (commit)" +fi OPTIONS+=( JOININBOX "Update the JoininBox scripts and menu" - JOINMARKET "Update/reinstall JoinMarket to $(grep testedJMversion= < ~/install.joinmarket.sh | cut -d '"' -f 2)") + JOINMARKET "Update/reinstall JoinMarket to ${testedVersion}") if [ "$runningEnv" = "standalone" ]; then OPTIONS+=(\ From 554a20a297ef9ca871f21d3c40c3a17ca5514070 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 29 Mar 2026 10:03:52 +0200 Subject: [PATCH 4/9] fix(stats): improve YGnickname function to handle log retrieval and output more robustly (#180) --- scripts/_functions.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/_functions.sh b/scripts/_functions.sh index ea5777c..c215143 100755 --- a/scripts/_functions.sh +++ b/scripts/_functions.sh @@ -170,16 +170,16 @@ function stopYG() { function YGnickname() { # Retrieves nickname from the latest NickServ message in the newest logfile - if ls -td /home/joinmarket/.joinmarket/logs/* 1>&2>/dev/null ; then - newest_log=$(ls -td /home/joinmarket/.joinmarket/logs/* | grep J5 | head -n 1) - name=$(grep NickServ $newest_log | tail -1 | awk '{print $9}') - if [ ${#name} -eq 0 ];then + newest_log=$(ls -td /home/joinmarket/.joinmarket/logs/*J5* 2>/dev/null | head -n 1) + if [ -n "$newest_log" ] && [ -f "$newest_log" ]; then + name=$(grep NickServ "$newest_log" 2>/dev/null | tail -1 | awk '{print $9}') + if [ ${#name} -eq 0 ]; then name="no_Nick_see_LOGS" fi else name="waiting__to__run" fi - echo $name + echo "$name" } # copyJoininboxScripts From efdf6609694e1615f6f00edd65106d0ecd144f58 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:31:05 +0200 Subject: [PATCH 5/9] fix(build): use rolling Debian ISO, verify ISO checksum with PGP, add packer syntax checks (#181) --- .github/workflows/packer-syntax-check.yml | 60 +++++++++++++ ci/amd64/debian/build.amd64-debian.pkr.hcl | 11 ++- ci/amd64/packer.build.amd64-debian.sh | 97 +++++++++++++++++++++- typos.toml | 3 +- 4 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/packer-syntax-check.yml diff --git a/.github/workflows/packer-syntax-check.yml b/.github/workflows/packer-syntax-check.yml new file mode 100644 index 0000000..d6dbd6f --- /dev/null +++ b/.github/workflows/packer-syntax-check.yml @@ -0,0 +1,60 @@ +name: Packer Syntax Check + +concurrency: + group: packer-syntax-check-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +on: + push: + branches: [ "master" ] + paths: + - '.github/workflows/packer-syntax-check.yml' + - 'ci/amd64/**/*.pkr.hcl' + - 'ci/amd64/**/*.pkrvars.hcl' + - 'ci/amd64/packer.build.amd64-debian.sh' + - 'ci/arm64-rpi/**/*.pkr.hcl' + - 'ci/arm64-rpi/**/*.pkrvars.hcl' + - 'ci/arm64-rpi/arm64-rpi.sh' + pull_request: + branches: [ "master" ] + paths: + - '.github/workflows/packer-syntax-check.yml' + - 'ci/amd64/**/*.pkr.hcl' + - 'ci/amd64/**/*.pkrvars.hcl' + - 'ci/amd64/packer.build.amd64-debian.sh' + - 'ci/arm64-rpi/**/*.pkr.hcl' + - 'ci/arm64-rpi/**/*.pkrvars.hcl' + - 'ci/arm64-rpi/arm64-rpi.sh' + workflow_dispatch: + +jobs: + validate-packer-syntax: + name: Validate Packer Syntax + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Packer + uses: hashicorp/setup-packer@v3 + + - name: Show Packer version + run: packer version + + - name: Validate amd64 templates (syntax only) + shell: bash + run: | + set -euo pipefail + + mapfile -t templates < <(find ci/amd64 ci/arm64-rpi -type f \( -name '*.pkr.hcl' -o -name '*.pkrvars.hcl' \) | sort) + + if [ ${#templates[@]} -eq 0 ]; then + echo "No Packer templates found under ci/amd64 or ci/arm64-rpi" + exit 1 + fi + + for template in "${templates[@]}"; do + echo "Validating $template" + packer validate -syntax-only "$template" + done diff --git a/ci/amd64/debian/build.amd64-debian.pkr.hcl b/ci/amd64/debian/build.amd64-debian.pkr.hcl index d7892e4..f5a34d9 100644 --- a/ci/amd64/debian/build.amd64-debian.pkr.hcl +++ b/ci/amd64/debian/build.amd64-debian.pkr.hcl @@ -1,7 +1,12 @@ # images, checksums and signatures are at: # https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/ -variable "iso_name" { default = "debian-13.3.0-amd64-netinst.iso" } -variable "iso_checksum" { default = "c9f09d24b7e834e6834f2ffa565b33d6f1f540d04bd25c79ad9953bc79a8ac02" } +# NOTE: This template is intended to be invoked via the wrapper script: +# ci/amd64/packer.build.amd64-debian.sh +# The wrapper resolves and injects the latest point-release ISO name and matching checksum +# at runtime. Defaults below are placeholders and are not guaranteed to work if you run +# `packer build` directly. +variable "iso_name" { default = "debian-13-amd64-netinst.iso" } +variable "iso_checksum" { default = "file:https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA256SUMS" } variable "github_user" { default = "openoms" } variable "branch" { default = "master" } @@ -57,7 +62,7 @@ source "qemu" "debian" { disk_size = var.image_size http_directory = "./http" iso_checksum = var.iso_checksum - iso_url = "https://cdimage.debian.org/cdimage/release/current/amd64/iso-cd/${var.iso_name}" + iso_url = "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/${var.iso_name}" memory = var.memory output_directory = "../builds/${local.name_template}-qemu" shutdown_command = "echo 'joininbox' | sudo /sbin/shutdown -hP now" diff --git a/ci/amd64/packer.build.amd64-debian.sh b/ci/amd64/packer.build.amd64-debian.sh index bf7a0f4..776410d 100644 --- a/ci/amd64/packer.build.amd64-debian.sh +++ b/ci/amd64/packer.build.amd64-debian.sh @@ -31,7 +31,102 @@ else branch=master fi -vars="-var github_user=${github_user} -var branch=${branch}" +# Resolve latest Debian 13 amd64 netinst ISO from SHA256SUMS. +# This avoids 404s and checksum mismatches when Debian point releases rotate. +debian_major=${DEBIAN_MAJOR:-13} +debian_iso_dir="https://cdimage.debian.org/debian-cd/current/amd64/iso-cd" +debian_sums_url="${debian_iso_dir}/SHA256SUMS" +debian_sums_sig_url="${debian_iso_dir}/SHA256SUMS.sign" +debian_cd_key_urls=( + "https://www.debian.org/CD/key-DA87E80D6294BE9B.txt" + "https://www.debian.org/CD/key-988021A964E6EA7D.txt" +) +debian_cd_expected_fprs=( + "DF9B9C49EAA9298432589D76DA87E80D6294BE9B" + "10460DAD76165AD81FBC0CE9988021A964E6EA7D" +) + +if ! command -v gpgv >/dev/null 2>&1; then + echo "# Installing gpgv" + sudo apt-get install -y gpgv +fi + +if ! command -v gpgv >/dev/null 2>&1; then + echo "ERROR: gpgv is required for signature verification" + exit 1 +fi + +tmp_checksums_dir=$(mktemp -d) +trap 'rm -rf "${tmp_checksums_dir}"' EXIT + +echo "# Downloading checksum files" +curl -fsSL "${debian_sums_url}" -o "${tmp_checksums_dir}/SHA256SUMS" +curl -fsSL "${debian_sums_sig_url}" -o "${tmp_checksums_dir}/SHA256SUMS.sign" + +echo "# Verifying SHA256SUMS signature (PGP)" +cd_keyring="${tmp_checksums_dir}/debian-cd-signing-keys.gpg" +tmp_gnupg_home="${tmp_checksums_dir}/gnupg-home" +mkdir -p "${tmp_gnupg_home}" +chmod 700 "${tmp_gnupg_home}" + +for i in "${!debian_cd_key_urls[@]}"; do + key_url="${debian_cd_key_urls[$i]}" + expected_fpr="${debian_cd_expected_fprs[$i]}" + key_file="${tmp_checksums_dir}/cd-key-${i}.asc" + curl -fsSL "${key_url}" -o "${key_file}" + actual_fpr=$(gpg --homedir "${tmp_gnupg_home}" --show-keys --with-colons "${key_file}" 2>/dev/null | awk -F: '/^fpr:/ {print $10; exit}') + if [ -z "${actual_fpr}" ] || [ "${actual_fpr}" != "${expected_fpr}" ]; then + echo "# SHA256SUMS signature: FAIL" + echo "ERROR: Unexpected fingerprint for ${key_url}" + exit 1 + fi + gpg --homedir "${tmp_gnupg_home}" --no-default-keyring --keyring "${cd_keyring}" --import "${key_file}" >/dev/null 2>&1 +done + +if gpgv --keyring "${cd_keyring}" "${tmp_checksums_dir}/SHA256SUMS.sign" "${tmp_checksums_dir}/SHA256SUMS" >/dev/null 2>&1; then + echo "# SHA256SUMS signature: PASS" +else + echo "# SHA256SUMS signature: FAIL" + echo "ERROR: PGP signature verification failed for ${debian_sums_url}" + exit 1 +fi + +echo "# Resolving latest Debian ${debian_major} amd64 netinst ISO from ${debian_sums_url}" +latest_iso_line=$(awk -v major="${debian_major}" '$2 ~ ("^\\*?\\.?/?debian-" major "\\.[0-9]+\\.[0-9]+-amd64-netinst\\.iso$") {print $1 " " $2}' "${tmp_checksums_dir}/SHA256SUMS" | \ + sort -k2 -V | tail -1) + +if [ -z "${latest_iso_line}" ]; then + echo "ERROR: Could not resolve latest Debian ${debian_major} amd64 netinst ISO from ${debian_sums_url}" + exit 1 +fi + +latest_iso_checksum=$(echo "${latest_iso_line}" | awk '{print $1}') +latest_iso_name=$(echo "${latest_iso_line}" | awk '{print $2}' | sed 's#^\*##; s#^\./##; s#^/##') + +if [ -z "${latest_iso_name}" ] || [ -z "${latest_iso_checksum}" ]; then + echo "ERROR: Failed parsing ISO name/checksum from: ${latest_iso_line}" + exit 1 +fi + +resolved_checksum=$(awk -v iso="${latest_iso_name}" '($2 == iso || $2 == "*" iso || $2 == "./" iso || $2 == "/" iso) {print $1; exit}' "${tmp_checksums_dir}/SHA256SUMS") +if [ -z "${resolved_checksum}" ]; then + echo "ERROR: Could not find checksum entry for ${latest_iso_name} in ${debian_sums_url}" + exit 1 +fi + +echo "# Debian ISO selection" +echo "# ISO filename : ${latest_iso_name}" +echo "# SHA256 (selected) : ${latest_iso_checksum}" +echo "# SHA256 (resolved) : ${resolved_checksum}" +if [ "${latest_iso_checksum}" = "${resolved_checksum}" ]; then + echo "# Checksum verify : PASS" +else + echo "# Checksum verify : FAIL" + echo "ERROR: Checksum mismatch for ${latest_iso_name}" + exit 1 +fi + +vars="-var github_user=${github_user} -var branch=${branch} -var iso_name=${latest_iso_name} -var iso_checksum=${latest_iso_checksum}" # Build the image echo "# Build the image with: github_user=${github_user} branch=${branch}" diff --git a/typos.toml b/typos.toml index 46ea55e..b2ca07e 100644 --- a/typos.toml +++ b/typos.toml @@ -7,4 +7,5 @@ [default.extend-words] # don't correct these false positives ba = "ba" -ned = "ned" \ No newline at end of file +ned = "ned" +fpr = "fpr" From aa8d79c97dbc8f5fc39adc96b9784fcfa868be91 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 29 Mar 2026 22:36:56 +0200 Subject: [PATCH 6/9] fix(service): add memory and CPU limits to reduce OOM kills (#182) --- scripts/start.service.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/start.service.sh b/scripts/start.service.sh index 1156475..a1b4c61 100755 --- a/scripts/start.service.sh +++ b/scripts/start.service.sh @@ -43,6 +43,18 @@ Type=simple TimeoutSec=infinity Restart=no +# Memory limits (DoS / OOM protection) +MemoryHigh=300M +MemoryMax=512M +MemorySwapMax=0 + +# Reduce OOM kill priority (lower = less likely to be killed) +OOMScoreAdjust=-500 +OOMPolicy=stop + +# CPU limit +CPUQuota=80% + # Hardening measures PrivateTmp=true ProtectSystem=full From dfb33c8ab074fd79a1dd95e01d7ba6843444b1f3 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:36:15 +0200 Subject: [PATCH 7/9] chore(standalone): bitcoin core update to v29.2 (#184) --- scripts/_functions.bitcoincore.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/_functions.bitcoincore.sh b/scripts/_functions.bitcoincore.sh index ea03c49..e4f1786 100755 --- a/scripts/_functions.bitcoincore.sh +++ b/scripts/_functions.bitcoincore.sh @@ -9,7 +9,7 @@ joininConfPath="/home/joinmarket/joinin.conf" function downloadBitcoinCore() { # set version # https://bitcoincore.org/en/download/ - bitcoinVersion="26.1" + bitcoinVersion="29.2" if bitcoin-cli --version | grep $bitcoinVersion >/dev/null; then echo "# Bitcoin Core $bitcoinVersion is already installed" From 54473190c8278a023049f87ea4c5520fb26425e2 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:37:17 +0200 Subject: [PATCH 8/9] chore: joinmarket-clientserver update to v0.9.12, use AdamISZ new code signing key (#185) --- scripts/install.joinmarket.sh | 17 +++++++++-------- scripts/verify.git.sh | 21 ++++++++++++++------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/scripts/install.joinmarket.sh b/scripts/install.joinmarket.sh index 6be93d8..a54e35f 100755 --- a/scripts/install.joinmarket.sh +++ b/scripts/install.joinmarket.sh @@ -2,19 +2,20 @@ # https://github.com/JoinMarket-Org/joinmarket-clientserver/releases # Use tag for verification when available, otherwise use commit hash -# testedJMversion="v0.9.11" +testedJMversion="v0.9.12" # https://github.com/JoinMarket-Org/joinmarket-clientserver/commits/master/ # Only used if testedJMversion is empty or not set -testedJMcommit="ce32bafbb5d716bde61830f71266410249d43dbc" +#testedJMcommit="ce32bafbb5d716bde61830f71266410249d43dbc" -PGPsigner="kristapsk" -PGPpkeys="https://github.com/kristapsk.gpg" -PGPcheck="33E472FE870C7E5D" +PGPsigner="AdamISZ" +PGPpkeys="https://github.com/AdamISZ.gpg" +# AdamISZ current primary code-signing key used for v0.9.12 +PGPcheck="0F1C7345D9193D1C8E3F21810C44134F93234873" -#PGPsigner="waxwing" -#PGPpkeys="https://raw.githubusercontent.com/JoinMarket-Org/joinmarket-clientserver/master/pubkeys/AdamGibson.asc" -#PGPcheck="2B6FC204D9BF332D062B461A141001A1AF77F20B" +#PGPsigner="kristapsk" +#PGPpkeys="https://github.com/kristapsk.gpg" +#PGPcheck="33E472FE870C7E5D" me="${0##/*}" diff --git a/scripts/verify.git.sh b/scripts/verify.git.sh index 89ded16..6360073 100644 --- a/scripts/verify.git.sh +++ b/scripts/verify.git.sh @@ -30,9 +30,18 @@ PGPsigner="$1" PGPpubkeyLink="$2" PGPpubkeyFingerprint="$3" -wget --prefer-family=ipv4 -O /dev/shm/pgp_keys_${PGPsigner}.asc "${PGPpubkeyLink}" -gpg --import --import-options show-only /dev/shm/pgp_keys_${PGPsigner}.asc -fingerprint=$(gpg --show-keys /dev/shm/pgp_keys_${PGPsigner}.asc 2>/dev/null | grep "${PGPpubkeyFingerprint}" -c) +_temp_dir="$(mktemp -d -p /dev/shm/ 2>/dev/null || mktemp -d)" +trap 'rm -rf "$_temp_dir"' EXIT + +keyFile="${_temp_dir}/pgp_keys_${PGPsigner}.asc" +rawKeyFile="${keyFile}.raw" + +wget --prefer-family=ipv4 -O "${rawKeyFile}" "${PGPpubkeyLink}" +# GitHub can add a Note: armor header when an account key cannot be exported. +# GPG imports the key anyway, but prints a misleading "unknown armor header". +grep -v '^Note: ' "${rawKeyFile}" >"${keyFile}" +gpg --quiet --import --import-options show-only "${keyFile}" +fingerprint=$(gpg --show-keys --with-subkey-fingerprint "${keyFile}" 2>/dev/null | tr -d " \t\n\r" | grep "${PGPpubkeyFingerprint}" -c) if [ "${fingerprint}" -lt 1 ]; then echo echo "# WARNING --> the PGP fingerprint is not as expected for ${PGPsigner}" >&2 @@ -40,11 +49,9 @@ if [ "${fingerprint}" -lt 1 ]; then echo "# Exiting" >&2 exit 7 fi -gpg --import /dev/shm/pgp_keys_${PGPsigner}.asc -rm /dev/shm/pgp_keys_${PGPsigner}.asc +gpg --quiet --import "${keyFile}" -trap 'rm -f "$_temp"' EXIT -_temp="$(mktemp -p /dev/shm/)" +_temp="${_temp_dir}/git-verify.out" if [ $# -eq 3 ] || [ -z "$4" ]; then commitHash="$(git log --oneline | head -1 | awk '{print $1}')" From 16fc83f47d119b4966944a7d707ef604874b2afa Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:57:42 +0200 Subject: [PATCH 9/9] chore: migrate the watch-only Bitcoin Core wallet to use descriptors (#177) * chore: update wallet creation to use watch-only-descriptor-wallet * remove duplicate version check * fix: update release URL in install script and correct typo in wallet import message * ci: test amd64 image descriptor wallet migration with Bats * ci: boot amd64 image tests with OVMF pflash * fix: gate descriptor wallet migration on Bitcoin Core v30 Detect the connected Bitcoin Core version over RPC before migrating. Keep wallet.dat for v29.x or when the version cannot be determined. Add regression coverage for both migration and compatibility paths. * ci: make amd64 image tests independent of guest apt Inject a pinned bats-core checkout into the temporary VM instead of installing Bats through the guest package repositories. Remove the duplicate pull request trigger and simplify artifact lookup. * ci: harden image build run * docs(FAQ): how automatic migration works --- .github/workflows/amd64-image-test.yml | 65 ++++++ FAQ.md | 70 ++++++- README.md | 2 +- ci/amd64/test.amd64-image-bats.sh | 138 +++++++++++++ scripts/_functions.bitcoincore.sh | 156 +++++++++++--- scripts/_functions.sh | 4 +- scripts/install.bitcoincore.sh | 14 +- scripts/install.joinmarket.sh | 3 +- scripts/menu.wallet.sh | 2 +- scripts/standalone/_functions.standalone.sh | 6 +- test/README.md | 34 ++++ test/bats/descriptor-wallet.bats | 214 ++++++++++++++++++++ test/run-bats-local.sh | 11 + 13 files changed, 675 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/amd64-image-test.yml create mode 100755 ci/amd64/test.amd64-image-bats.sh create mode 100644 test/README.md create mode 100644 test/bats/descriptor-wallet.bats create mode 100755 test/run-bats-local.sh diff --git a/.github/workflows/amd64-image-test.yml b/.github/workflows/amd64-image-test.yml new file mode 100644 index 0000000..bc403df --- /dev/null +++ b/.github/workflows/amd64-image-test.yml @@ -0,0 +1,65 @@ +name: amd64-image-test + +concurrency: + group: amd64-image-test-${{ github.event.workflow_run.id || inputs.run_id || github.run_id }} + cancel-in-progress: true + +on: + workflow_run: + workflows: ["amd64-image-build"] + types: [completed] + workflow_dispatch: + inputs: + run_id: + description: "amd64-image-build workflow run ID to test" + required: true + type: string + +permissions: + actions: read + contents: read + +jobs: + bats-image-test: + name: Run Bats against amd64 image artifact + runs-on: ubuntu-22.04 + timeout-minutes: 240 + if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository) }} + + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ github.event.workflow_run.head_repository.full_name || github.repository }} + ref: ${{ github.event.workflow_run.head_sha || github.ref }} + + - name: Check out bats-core + uses: actions/checkout@v4 + with: + repository: bats-core/bats-core + # bats-core v1.12.0 + ref: 713504bc0224a19b3d7c7958c18dc07f64f54b44 + path: .bats-core + persist-credentials: false + + - name: Download amd64 image artifact + uses: actions/download-artifact@v4 + with: + github-token: ${{ github.token }} + run-id: ${{ github.event.workflow_run.id || inputs.run_id }} + pattern: joininbox-amd64-image-* + path: artifacts + merge-multiple: true + + - name: Verify and decompress image + shell: bash + run: | + set -euo pipefail + + cd artifacts + sha256sum -c joininbox-amd64-debian.qcow2.gz.sha256 + gzip -dk joininbox-amd64-debian.qcow2.gz + sha256sum -c joininbox-amd64-debian.qcow2.sha256 + + - name: Run image Bats tests + timeout-minutes: 30 + run: ci/amd64/test.amd64-image-bats.sh "${GITHUB_WORKSPACE}/artifacts/joininbox-amd64-debian.qcow2" diff --git a/FAQ.md b/FAQ.md index ae14865..db608dd 100644 --- a/FAQ.md +++ b/FAQ.md @@ -30,6 +30,7 @@ - [Wallet recovery](#wallet-recovery) - [on JoininBox](#on-joininbox) - [on the remote node](#on-the-remote-node) +- [Migrating from legacy wallet.dat to descriptor wallet](#migrating-from-legacy-walletdat-to-descriptor-wallet) - [USB SSD recommendation](#usb-ssd-recommendation) - [Pruned node notes](#pruned-node-notes) - [External drive](#external-drive) @@ -195,7 +196,7 @@ Use `ssh` with `torsocks` on the desktop (needs Tor installed): server=1 daemon=1 disablewallet=0 -main.wallet=wallet.dat +main.wallet=watch-only-descriptor-wallet # Connection settings rpcuser=REDACTED @@ -428,7 +429,7 @@ JoinMarket docs: * https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/USAGE.md#recover ### on JoininBox -* Connect the remote bitcoind with `CONFIG` -> `CONNECT` menu so it checks if the connection is successful. It will also set the remote watch-only wallet in bitcoind to "joininbox" so will need to rescan that after recovering an old wallet with previously used addresses. +* Connect the remote bitcoind with `CONFIG` -> `CONNECT` menu so it checks if the connection is successful. It will also set the remote watch-only-descriptor-wallet in bitcoind to "joininbox" so will need to rescan that after recovering an old wallet with previously used addresses. * When using the CLI and connecting to the remote node over Tor, you will need to use the script with the torsocks prefix like: `torsocks python3 wallet-tool.py --recoversync -g 20 ~/.joinmarket/wallets/wallet.jmdat` @@ -437,7 +438,7 @@ JoinMarket docs: * Use the menu option `WALLET` -> `RESCAN` or follow manually * the wallet defined as `rpc_wallet =` -in the joinmarket.cfg is the wallet which is used as watch only in the remote bitcoind. +in the joinmarket.cfg is the wallet which is used as a watch-only-descriptor-wallet in the remote bitcoind. You need to run rescanblockchain on that wallet in bitcoind after importing the joinmarket wallet. * The wallet is set in the joinmarket.cfg (by default called `joininbox` should show up when you run: `bitcoin-cli listwallets` @@ -449,6 +450,65 @@ Rescanning from the first SegWit block is sufficient for the default SegWit wall * Monitor progress (on a RaspiBlitz): `sudo tail -fn 100 /mnt/hdd/app-storage/bitcoin/debug.log` Once the rescan is finished you balances should appear in the `INFO` menu (`wallet-tool.py`) + +## Migrating from legacy wallet.dat to descriptor wallet + +Starting with the 0.9.0 version, JoininBox uses Bitcoin Core's descriptor wallets (`watch-only-descriptor-wallet`) instead of the legacy `wallet.dat`. This change provides better compatibility with modern Bitcoin Core versions (v26+) and aligns with Bitcoin Core's default wallet format. + +### Why this change? + +Bitcoin Core has deprecated BDB (Berkeley DB) wallets in favor of descriptor wallets. The new descriptor wallets: +- Are the default in Bitcoin Core v26+ +- Don't require the `deprecatedrpc=create_bdb` configuration +- Have better performance and features +- Are actively maintained and improved + +### How automatic migration works + +When `rpc_wallet_file` is still set to `wallet.dat`, JoininBox queries the connected Bitcoin Core node using RPC before changing the configuration: + +- Bitcoin Core v30.0 and newer: JoininBox atomically changes `rpc_wallet_file` to `watch-only-descriptor-wallet`, creates or loads that descriptor wallet, and displays the migration notice when the old `wallet.dat` is present. +- Bitcoin Core v29.x and earlier: JoininBox keeps using `wallet.dat` and does not start automatic migration. +- Version unavailable: JoininBox leaves `wallet.dat` configured rather than migrating without confirming compatibility. + +The migration does not rename, modify, or delete the old `wallet.dat`. It changes which Bitcoin Core wallet JoinMarket uses for watch-only address imports and transaction history. + +### Migration steps for existing users + +If you're upgrading from a previous version of JoininBox that used `wallet.dat`, follow these steps: + +1. **The migration notice will appear automatically on Bitcoin Core v30.0 and newer** + When you first use any wallet-related function after updating, JoininBox checks the connected Bitcoin Core version. On v30.0 and newer it switches the configured RPC wallet to `watch-only-descriptor-wallet`, detects the old `wallet.dat`, and displays a migration notice. Bitcoin Core v29.x and earlier continue using `wallet.dat` without starting the automatic migration. + +2. **Open each JoinMarket wallet** + Go to `WALLET` -> `DISPLAY` and open each of your JoinMarket wallets (`.jmdat` files) at least once. This imports the addresses into the new `watch-only-descriptor-wallet` in Bitcoin Core. + +3. **Run a blockchain rescan** + After opening all your wallets, go to `WALLET` -> `RESCAN` and enter a blockheight: + - Use `481824` (first SegWit block) for wallets created after August 2017 + - Can use a later blockheight if you know when your wallet had its first deposit + +4. **Wait for the rescan to complete** + The rescan can take several hours depending on the blockheight and your hardware. Monitor progress with: + ```bash + # On standalone JoininBox: + sudo tail -f /home/bitcoin/.bitcoin/debug.log + + # On RaspiBlitz: + sudo tail -f /mnt/hdd/bitcoin/debug.log + ``` + +5. **Verify your balances** + Once the rescan completes, check your wallet balances with `WALLET` -> `DISPLAY`. + +### Notes + +- The old `wallet.dat` is not deleted and remains in Bitcoin Core +- Automatic migration only runs when the connected Bitcoin Core version is v30.0 or newer +- You only need to perform this migration once +- The migration notice will not appear again after you acknowledge it +- If you have issues, you can reset the migration flag by removing `walletMigrationDone=true` from `/home/joinmarket/joinin.conf` + ## USB SSD recommendation **JoininBox operates on the minimum viable hardware under the assumption that the seed (and passphrase) of the wallets used is safely backed up and can be recovered fully** * The above warning is especially true for SDcard as they fail often, use a good quality one. @@ -519,7 +579,7 @@ Alternatively to a pruned node there could be a larger >400 GB storage connected # -rw------- 1 bitcoin bitcoin 1521305 Mar 21 10:38 peers.dat # -rw-r--r-- 1 bitcoin bitcoin 7 Mar 21 10:08 settings.json # drwx------ 34 bitcoin bitcoin 4096 Dec 7 23:39 specter - # drwx------ 2 bitcoin bitcoin 4096 Mar 21 10:38 wallet.dat + # drwx------ 2 bitcoin bitcoin 4096 Mar 21 10:38 watch-only-descriptor-wallet installMainnet ... # # OK - the bitcoind.service is now enabled @@ -528,7 +588,7 @@ Alternatively to a pruned node there could be a larger >400 GB storage connected # # # Monitor the bitcoind with: sudo tail -f /home/bitcoin/.bitcoin/mainnet/debug.log # - # # Create wallet.dat ... + # # Create watch-only-descriptor-wallet ... # error code: -28 # error message: # Loading block index... diff --git a/README.md b/README.md index a9f254f..f9e7ef4 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ A minimalistic, security focused linux environment for JoinMarket with a termina * Start a pruned node from https://pruned.host4coins.net/blocks * JoininBox is part of the RaspiBlitz SERVICES -**The addresses, transactions and balances of JoinMarket can be seen in the watch-only wallet of the connected node.** +**The addresses, transactions and balances of JoinMarket can be seen in the watch-only-descriptor-wallet of the connected node.** * use your own or a trusted node * to protect privacy in case of physical access use disk encryption diff --git a/ci/amd64/test.amd64-image-bats.sh b/ci/amd64/test.amd64-image-bats.sh new file mode 100755 index 0000000..5313656 --- /dev/null +++ b/ci/amd64/test.amd64-image-bats.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -euo pipefail + +image="${1:-${GITHUB_WORKSPACE:-$(pwd)}/ci/amd64/builds/joininbox-amd64-debian-qemu/joininbox-amd64-debian.qcow2}" +ssh_port="${SSH_PORT:-2222}" +ssh_password="${SSH_PASSWORD:-joininbox}" +qemu_pid_file="${RUNNER_TEMP:-/tmp}/joininbox-qemu.pid" +bats_core_dir="${BATS_CORE_DIR:-${GITHUB_WORKSPACE:-$(pwd)}/.bats-core}" + +if [ ! -f "${image}" ]; then + echo "Missing image: ${image}" >&2 + exit 1 +fi + +if [ ! -x "${bats_core_dir}/bin/bats" ]; then + echo "Missing bats-core checkout: ${bats_core_dir}" >&2 + exit 1 +fi + +sudo apt-get update +sudo apt-get install -y ovmf qemu-system-x86 sshpass + +ssh_opts=( + -o StrictHostKeyChecking=no + -o UserKnownHostsFile=/dev/null + -o ConnectTimeout=5 + -p "${ssh_port}" +) + +ovmf_code="${OVMF_CODE:-${OVMF_BIOS:-}}" +ovmf_vars_template="${OVMF_VARS:-}" +ovmf_vars="${RUNNER_TEMP:-/tmp}/joininbox-ovmf-vars.fd" +qemu_firmware_args=() + +if [ -z "${ovmf_code}" ]; then + for candidate in \ + /usr/share/OVMF/OVMF_CODE_4M.fd \ + /usr/share/OVMF/OVMF_CODE_4M.secboot.fd \ + /usr/share/OVMF/OVMF_CODE_4M.ms.fd \ + /usr/share/OVMF/OVMF_CODE.fd \ + /usr/share/OVMF/OVMF.fd \ + /usr/share/ovmf/OVMF_CODE_4M.fd \ + /usr/share/ovmf/OVMF_CODE_4M.secboot.fd \ + /usr/share/ovmf/OVMF_CODE_4M.ms.fd \ + /usr/share/ovmf/OVMF_CODE.fd \ + /usr/share/ovmf/OVMF.fd \ + OVMF.fd; do + if [ -f "${candidate}" ]; then + ovmf_code="${candidate}" + break + fi + done +fi + +if [ -z "${ovmf_code}" ] || [ ! -f "${ovmf_code}" ]; then + echo "No OVMF firmware found. Set OVMF_CODE or OVMF_BIOS to the firmware path." >&2 + find /usr/share/OVMF /usr/share/ovmf -maxdepth 1 -type f -name '*.fd' -print 2>/dev/null || true + exit 1 +fi + +case "${ovmf_code##*/}" in + *CODE*) + if [ -z "${ovmf_vars_template}" ]; then + for candidate in \ + "${ovmf_code/CODE/VARS}" \ + /usr/share/OVMF/OVMF_VARS_4M.fd \ + /usr/share/OVMF/OVMF_VARS.fd \ + /usr/share/ovmf/OVMF_VARS_4M.fd \ + /usr/share/ovmf/OVMF_VARS.fd; do + if [ -f "${candidate}" ]; then + ovmf_vars_template="${candidate}" + break + fi + done + fi + + if [ -z "${ovmf_vars_template}" ] || [ ! -f "${ovmf_vars_template}" ]; then + echo "No OVMF VARS template found for ${ovmf_code}. Set OVMF_VARS to the template path." >&2 + find /usr/share/OVMF /usr/share/ovmf -maxdepth 1 -type f -name '*.fd' -print 2>/dev/null || true + exit 1 + fi + + cp "${ovmf_vars_template}" "${ovmf_vars}" + qemu_firmware_args=( + -drive "if=pflash,format=raw,readonly=on,file=${ovmf_code}" + -drive "if=pflash,format=raw,file=${ovmf_vars}" + ) + ;; + *) + qemu_firmware_args=(-bios "${ovmf_code}") + ;; +esac + +cleanup() { + if [ -f "${qemu_pid_file}" ]; then + qemu_pid="$(cat "${qemu_pid_file}")" + if kill -0 "${qemu_pid}" 2>/dev/null; then + kill "${qemu_pid}" 2>/dev/null || true + timeout 30s tail --pid="${qemu_pid}" -f /dev/null 2>/dev/null || + kill -9 "${qemu_pid}" 2>/dev/null || + true + fi + fi +} +trap cleanup EXIT + +rm -f "${qemu_pid_file}" + +qemu-system-x86_64 \ + -m 2048 \ + -smp 2 \ + "${qemu_firmware_args[@]}" \ + -drive "file=${image},format=qcow2" \ + -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:${ssh_port}-:22" \ + -device e1000,netdev=net0 \ + -display none \ + -snapshot \ + -pidfile "${qemu_pid_file}" \ + -daemonize + +echo "Waiting for SSH in the booted image" +for attempt in {1..120}; do + if sshpass -p "${ssh_password}" ssh "${ssh_opts[@]}" joinmarket@127.0.0.1 "true" 2>/dev/null; then + break + fi + if [ "${attempt}" -eq 120 ]; then + echo "Timed out waiting for SSH" >&2 + exit 1 + fi + sleep 5 +done + +tar -C "${bats_core_dir}" -cf - . | + sshpass -p "${ssh_password}" ssh "${ssh_opts[@]}" joinmarket@127.0.0.1 \ + "mkdir -p /tmp/bats-core && tar -C /tmp/bats-core -xf -" + +sshpass -p "${ssh_password}" ssh "${ssh_opts[@]}" joinmarket@127.0.0.1 \ + "PATH=/tmp/bats-core/bin:\$PATH /home/joinmarket/joininbox/test/run-bats-local.sh" diff --git a/scripts/_functions.bitcoincore.sh b/scripts/_functions.bitcoincore.sh index e4f1786..4c1988c 100755 --- a/scripts/_functions.bitcoincore.sh +++ b/scripts/_functions.bitcoincore.sh @@ -247,8 +247,8 @@ setJMconfigToSignet() { sed -i "s/^rpc_password =.*/rpc_password = $RPCPWSIGNET/g" $JMcfgPath echo "# rpc_password = $RPCPWSIGNET" # rpc_wallet_file - sed -i "s/^rpc_wallet_file =.*/rpc_wallet_file = wallet.dat/g" $JMcfgPath - echo "# using the bitcoind wallet: wallet.dat" + sed -i "s/^rpc_wallet_file =.*/rpc_wallet_file = watch-only-descriptor-wallet/g" $JMcfgPath + echo "# using the bitcoind watch-only-descriptor-wallet" # rpc_host sed -i "s/^rpc_host =.*/rpc_host = 127.0.0.1/g" $JMcfgPath echo "# rpc_host = 127.0.0.1" @@ -321,38 +321,79 @@ function getRPC { fi } +# getConnectedBitcoinCoreVersion - read the numeric version over node RPC +function getConnectedBitcoinCoreVersion { + local tor="" + if [ "$(echo "$rpc_host" | grep -c .onion)" -gt 0 ]; then + tor="torsocks" + fi + $tor curl -sS --data-binary \ + '{"jsonrpc": "1.0", "id":"get_bitcoin_core_version", "method": "getnetworkinfo", "params": []}' \ + "http://$rpc_user:$rpc_pass@$rpc_host:$rpc_port/" 2>/dev/null | + jq -r '.result.version // empty' 2>/dev/null +} + +# migrateLegacyRPCWalletConfig - switch the persisted JoinMarket RPC wallet +# from the legacy wallet.dat name to the descriptor wallet used by JoininBox +# when the connected Bitcoin Core version is v30.0 or newer +function migrateLegacyRPCWalletConfig { + if [ "$rpc_wallet" != "wallet.dat" ]; then + return 0 + fi + + local bitcoinCoreVersion + bitcoinCoreVersion=$(getConnectedBitcoinCoreVersion) + if ! [[ "$bitcoinCoreVersion" =~ ^[0-9]+$ ]]; then + echo "# Could not determine the connected Bitcoin Core version; keeping wallet.dat" + return 0 + fi + # Bitcoin Core's numeric version is 290200 for v29.2 and 300000 for v30.0. + if [ "$bitcoinCoreVersion" -lt 300000 ]; then + echo "# Connected Bitcoin Core is v29.x or earlier; keeping wallet.dat" + return 0 + fi + + echo "# Migrating the configured Bitcoin Core wallet from wallet.dat to watch-only-descriptor-wallet" + local migrationConfigOutput + if ! migrationConfigOutput=$(mktemp "${JMcfgPath}.XXXXXX"); then + echo "# Failed to create a temporary descriptor wallet configuration" >&2 + return 1 + fi + if ! sed \ + "s/^rpc_wallet_file =.*/rpc_wallet_file = watch-only-descriptor-wallet/g" \ + "$JMcfgPath" >"$migrationConfigOutput"; then + rm -f "$migrationConfigOutput" + echo "# Failed to prepare the descriptor wallet configuration" >&2 + return 1 + fi + if ! mv "$migrationConfigOutput" "$JMcfgPath"; then + rm -f "$migrationConfigOutput" + echo "# Failed to update the descriptor wallet configuration" >&2 + return 1 + fi + getRPC + if [ "$rpc_wallet" != "watch-only-descriptor-wallet" ]; then + echo "# Failed to select the descriptor wallet configuration" >&2 + return 1 + fi +} + # checkRPCwallet function checkRPCwallet { getRPC + migrateLegacyRPCWalletConfig || return 1 if [ $# -eq 0 ]; then rpc_wallet=$rpc_wallet else rpc_wallet=$1 fi - echo "# Check 'deprecatedrpc=create_bdb' in bitcoin.conf" - source ${joininConfPath} - if [ $runningEnv = standalone ]; then - bitcoinConfPath="/home/bitcoin/.bitcoin/bitcoin.conf" - elif [ $runningEnv = raspiblitz ]; then - if [ -f "/mnt/hdd/raspiblitz.conf" ]; then - bitcoinConfPath="/mnt/hdd/bitcoin/bitcoin.conf" - else - bitcoinConfPath="/mnt/hdd/app-data/bitcoin/bitcoin.conf" - fi - fi - if ! sudo grep -c "deprecatedrpc=create_bdb" "$bitcoinConfPath"; then - echo "# Place 'deprecatedrpc=create_bdb' in bitcoin.conf" - echo "deprecatedrpc=create_bdb" | sudo tee -a "$bitcoinConfPath" - echo "# Restarting bitcoind" - sudo systemctl restart bitcoind - fi echo "# Making sure the set $rpc_wallet wallet is present in bitcoind" trap 'rm -f "$connectionOutput"' EXIT connectionOutput=$(mktemp -p /dev/shm/) walletFound=$(customRPC "# Check wallet" "listwallets" 2>$connectionOutput | grep -c "$rpc_wallet") if [ $walletFound -eq 0 ]; then - echo "# Setting a watch only wallet in Bitcoin Core named $rpc_wallet" + echo "# Setting a watch-only-descriptor-wallet in Bitcoin Core named $rpc_wallet" tor="" if [ $(echo $rpc_host | grep -c .onion) -gt 0 ]; then tor="torsocks" @@ -361,8 +402,8 @@ function checkRPCwallet { fi #TODO rewrite customRPC to support multiple params $tor curl -sS --data-binary \ - '{"jsonrpc": "1.0", "id":"# Create the bitcoind wallet", "method": "createwallet", "params": {"wallet_name":"'"$rpc_wallet"'","descriptors":false}}' \ - http://$rpc_user:$rpc_pass@$rpc_host:$rpc_port/wallet/$rpc_wallet | jq . + '{"jsonrpc": "1.0", "id":"# Create the bitcoind wallet", "method": "createwallet", "params": {"wallet_name":"'"$rpc_wallet"'","descriptors":true,"disable_private_keys":true}}' \ + http://$rpc_user:$rpc_pass@$rpc_host:$rpc_port/ | jq . echo walletFound=$(customRPC "# Check wallet" "listwallets" 2>$connectionOutput | grep -c "$rpc_wallet") if [ $walletFound -eq 0 ]; then @@ -374,6 +415,75 @@ function checkRPCwallet { echo fi echo "# The wallet: $rpc_wallet is present and loaded in the connected bitcoind" + + # Check for wallet migration from legacy wallet.dat + checkWalletMigration +} + +# checkWalletMigration - detects legacy wallet.dat and guides user through migration +# This function checks if: +# 1. The old wallet.dat exists in Bitcoin Core +# 2. The new watch-only-descriptor-wallet is being used +# 3. Migration has not been completed yet +# If migration is needed, it prompts the user to rescan after opening their JM wallets +function checkWalletMigration { + # Skip if migration was already completed + if grep -q "walletMigrationDone=true" "${joininConfPath}" 2>/dev/null; then + return 0 + fi + + # Skip if we're not using the new descriptor wallet + if [ "$rpc_wallet" != "watch-only-descriptor-wallet" ]; then + return 0 + fi + + # RPC settings are already available from parent checkRPCwallet function + tor="" + if [ "$(echo "$rpc_host" | grep -c .onion)" -gt 0 ]; then + tor="torsocks" + fi + + # Check if old wallet.dat exists in bitcoind (try to load it to see if it exists) + # First check listwalletdir for wallet.dat + oldWalletExists=$($tor curl -sS --data-binary \ + '{"jsonrpc": "1.0", "id":"check_old_wallet", "method": "listwalletdir", "params": []}' \ + "http://$rpc_user:$rpc_pass@$rpc_host:$rpc_port/" 2>/dev/null | jq -r '.result.wallets[].name' 2>/dev/null | grep -c "^wallet.dat$") + + if [ "$oldWalletExists" -gt 0 ]; then + echo + echo "########################################################################" + echo "# WALLET MIGRATION NOTICE" + echo "########################################################################" + echo + echo "# A legacy wallet.dat was detected in Bitcoin Core." + echo "# JoininBox now uses descriptor wallets (watch-only-descriptor-wallet)" + echo "# for better compatibility with modern Bitcoin Core versions." + echo + echo "# To complete the migration and see your transaction history:" + echo + echo "# 1. Open each of your JoinMarket wallets once using:" + echo "# WALLET -> DISPLAY" + echo "# This imports the addresses into the new descriptor wallet." + echo + echo "# 2. After opening all wallets, run a blockchain rescan:" + echo "# WALLET -> RESCAN" + echo "# Use blockheight 481824 (first SegWit block) or later if you know when your wallet had its first deposit." + echo + echo "# The rescan may take several hours depending on wallet age." + echo "# You can monitor progress in the Bitcoin Core debug.log" + echo + echo "########################################################################" + echo + echo "# Press ENTER to continue..." + read -r + + # Mark migration notice as shown (user can still run rescan manually) + if ! grep -q "walletMigrationDone=" "${joininConfPath}" 2>/dev/null; then + echo "walletMigrationDone=true" >>"${joininConfPath}" + else + sed -i "s/^walletMigrationDone=.*/walletMigrationDone=true/g" "${joininConfPath}" + fi + fi } # customRPC - sends a custom RPC command @@ -435,7 +545,7 @@ function connectLocalNode() { elif [ "${network}" = testnet ]; then rpc_port="18332" fi - rpc_wallet="wallet.dat" + rpc_wallet="watch-only-descriptor-wallet" if [ $runningEnv = raspiblitz ]; then if [ -f "/mnt/hdd/raspiblitz.conf" ]; then rpc_user=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcuser | cut -c 9-) diff --git a/scripts/_functions.sh b/scripts/_functions.sh index c215143..318634c 100755 --- a/scripts/_functions.sh +++ b/scripts/_functions.sh @@ -282,8 +282,8 @@ function generateJMconfig() { fi sed -i "s/^rpc_port =.*/rpc_port = $RPCPORT/g" $JMcfgPath echo "# rpc_port = $RPCPORT" - sed -i "s/^rpc_wallet_file =.*/rpc_wallet_file = wallet.dat/g" $JMcfgPath - echo "# using the bitcoind wallet: wallet.dat" + sed -i "s/^rpc_wallet_file =.*/rpc_wallet_file = watch-only-descriptor-wallet/g" $JMcfgPath + echo "# using the bitcoind watch-only-descriptor-wallet" # set joinin.conf value /home/joinmarket/set.value.sh set network mainnet ${joininConfPath} fi diff --git a/scripts/install.bitcoincore.sh b/scripts/install.bitcoincore.sh index b29da0c..170f058 100755 --- a/scripts/install.bitcoincore.sh +++ b/scripts/install.bitcoincore.sh @@ -5,13 +5,13 @@ source /home/joinmarket/_functions.sh # check connectedRemoteNode var in joinin.conf if ! grep -Eq "^connectedRemoteNode=" $joininConfPath; then - echo "connectedRemoteNode=off" >> $joininConfPath + echo "connectedRemoteNode=off" >>$joininConfPath fi if [ "$1" = "signetOn" ]; then installBitcoinCore installSignet - if [ "$connectedRemoteNode" = "on" ];then + if [ "$connectedRemoteNode" = "on" ]; then backupJMconf fi generateJMconfig @@ -24,16 +24,16 @@ if [ "$1" = "signetOn" ]; then bitcoinUser="joinmarket" cliPath="/home/joinmarket/bitcoin/" fi - if [ ! -f /home/${bitcoinUser}/.bitcoin/signet/wallets/wallet.dat/wallet.dat ];then - echo "# Create wallet.dat for signet ..." + if [ ! -d /home/${bitcoinUser}/.bitcoin/signet/wallets/watch-only-descriptor-wallet ]; then + echo "# Create watch-only-descriptor-wallet for signet ..." sleep 10 - sudo -u ${bitcoinUser} ${cliPath}/bitcoin-cli -signet -named createwallet wallet_name=wallet.dat descriptors=false + sudo -u ${bitcoinUser} ${cliPath}/bitcoin-cli -signet -named createwallet wallet_name=watch-only-descriptor-wallet descriptors=true disable_private_keys=true fi elif [ "$1" = "signetOff" ]; then removeSignetdService - isSignet=$(grep -c "network = signet" < $JMcfgPath) - if [ $isSignet -gt 0 ];then + isSignet=$(grep -c "network = signet" <$JMcfgPath) + if [ $isSignet -gt 0 ]; then echo "# Removing the joinmarket.cfg with signet settings" rm -f $JMcfgPath else diff --git a/scripts/install.joinmarket.sh b/scripts/install.joinmarket.sh index a54e35f..6cb3e81 100755 --- a/scripts/install.joinmarket.sh +++ b/scripts/install.joinmarket.sh @@ -120,9 +120,8 @@ range_argument install "install" "config" "update" "testPR" "commit" : "${version:=${testedJMversion:-$testedJMcommit}}" # Only check GitHub releases if version looks like a tag (starts with 'v') if [[ "${version}" == v* ]]; then - curl -s "https://github.com/JoinMarket-Org/joinmarket-clientserver/release/tag/${version}" | grep -q "\"message\": \"Version not found\"" && error_msg "'There is no: https://github.com/JoinMarket-Org/joinmarket-clientserver/release/tag/${version}'" + curl -s "https://github.com/JoinMarket-Org/joinmarket-clientserver/releases/tag/${version}" | grep -q "\"message\": \"Version not found\"" && error_msg "'There is no: https://github.com/JoinMarket-Org/joinmarket-clientserver/releases/tag/${version}'" fi - : "${qtgui:=false}" range_argument qtgui "0" "1" "false" "true" diff --git a/scripts/menu.wallet.sh b/scripts/menu.wallet.sh index 08e919f..ea2801f 100755 --- a/scripts/menu.wallet.sh +++ b/scripts/menu.wallet.sh @@ -194,7 +194,7 @@ Enter the new gap limit to be used" 16 60 2> "$gaplimit" echo /home/joinmarket/start.script.sh wallet-tool "$(cat $wallet)"|grep mixdepth|sed -n '1~2p'|awk '{print $3}' echo - echo "Import the master public keys to Specter Desktop or Electrum to create watch only wallets." + echo "Import the master public keys to Specter Desktop or Electrum to create watch-only wallets." echo echo "Press ENTER to return to the menu..." read key diff --git a/scripts/standalone/_functions.standalone.sh b/scripts/standalone/_functions.standalone.sh index a4108cb..d03ed66 100755 --- a/scripts/standalone/_functions.standalone.sh +++ b/scripts/standalone/_functions.standalone.sh @@ -290,9 +290,9 @@ WantedBy=multi-user.target echo "# Monitor the bitcoind with: sudo tail -f /home/bitcoin/.bitcoin/mainnet/debug.log" echo - if [ ! -f /home/bitcoin/.bitcoin/mainnet/wallets/wallet.dat/wallet.dat ]; then - echo "# Create wallet.dat ..." + if [ ! -d /home/bitcoin/.bitcoin/wallets/watch-only-descriptor-wallet ]; then + echo "# Create watch-only-descriptor-wallet ..." sleep 10 - sudo -u bitcoin /usr/local/bin/bitcoin-cli -named createwallet wallet_name=wallet.dat descriptors=false + sudo -u bitcoin /usr/local/bin/bitcoin-cli -named createwallet wallet_name=watch-only-descriptor-wallet descriptors=true disable_private_keys=true fi } diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..7196ac1 --- /dev/null +++ b/test/README.md @@ -0,0 +1,34 @@ +# JoininBox integration tests + +Run the local Bats suite with: + +```bash +test/run-bats-local.sh +``` + +The descriptor wallet tests require: + +- `bats` +- `bitcoind` +- `bitcoin-cli` +- `curl` +- `jq` + +The suite starts its own temporary `bitcoind -regtest` datadir and does not use +mainnet, signet, or any existing Bitcoin Core state. + +The `amd64-image-test` workflow downloads a previously built +`joininbox-amd64-image-*` artifact, verifies the compressed and raw checksums, +decompresses a runner-local qcow2 copy, boots it with QEMU in snapshot mode, +copies a pinned `bats-core` checkout into that temporary VM session, and runs +the same suite from the JoininBox checkout inside the image. This avoids +depending on the guest's configured APT repositories just to install test +tooling. + +The test workflow has two entry points: + +- `workflow_run`: runs after a successful `amd64-image-build` once this workflow + exists on the repository default branch. +- `workflow_dispatch`: reruns against a specific build artifact by providing the + `amd64-image-build` workflow run ID, as long as the artifact is still retained + by GitHub Actions. diff --git a/test/bats/descriptor-wallet.bats b/test/bats/descriptor-wallet.bats new file mode 100644 index 0000000..8cb1d9b --- /dev/null +++ b/test/bats/descriptor-wallet.bats @@ -0,0 +1,214 @@ +#!/usr/bin/env bats + +root_dir="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" +PATH="/home/joinmarket/bitcoin:/usr/local/bin:$PATH" + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + skip "$1 is required" + fi +} + +setup() { + require_command bitcoind + require_command bitcoin-cli + require_command curl + require_command jq + + rpc_user="joininbox" + rpc_pass="joininbox" + rpc_port="$((20000 + (RANDOM % 20000)))" + p2p_port="$((40000 + (RANDOM % 20000)))" + bitcoin_datadir="${BATS_TEST_TMPDIR}/bitcoin" + joinmarket_cfg="${BATS_TEST_TMPDIR}/joinmarket.cfg" + joinin_conf="${BATS_TEST_TMPDIR}/joinin.conf" + + mkdir -p "$bitcoin_datadir" + + bitcoind \ + -regtest \ + -datadir="$bitcoin_datadir" \ + -server \ + -daemonwait \ + -rpcuser="$rpc_user" \ + -rpcpassword="$rpc_pass" \ + -rpcport="$rpc_port" \ + -port="$p2p_port" \ + -fallbackfee=0.0001 + + cat >"$joinmarket_cfg" <"$joinin_conf" +} + +teardown() { + if [ -n "${bitcoin_datadir:-}" ] && [ -d "$bitcoin_datadir" ]; then + bitcoin-cli \ + -regtest \ + -datadir="$bitcoin_datadir" \ + -rpcuser="$rpc_user" \ + -rpcpassword="$rpc_pass" \ + -rpcport="$rpc_port" \ + stop >/dev/null 2>&1 || true + fi +} + +load_joininbox_bitcoin_functions() { + # shellcheck source=scripts/_functions.bitcoincore.sh + # shellcheck disable=SC1091 + source "$root_dir/scripts/_functions.bitcoincore.sh" + # shellcheck disable=SC2034 + JMcfgPath="$joinmarket_cfg" + # shellcheck disable=SC2034 + joininConfPath="$joinin_conf" + + mktemp() { + if [ "${1:-}" = "-p" ] && [ "${2:-}" = "/dev/shm/" ]; then + command mktemp "${BATS_TEST_TMPDIR}/joininbox.XXXXXX" + else + command mktemp "$@" + fi + } +} + +wallet_info() { + bitcoin-cli \ + -regtest \ + -datadir="$bitcoin_datadir" \ + -rpcuser="$rpc_user" \ + -rpcpassword="$rpc_pass" \ + -rpcport="$rpc_port" \ + -rpcwallet=watch-only-descriptor-wallet \ + getwalletinfo +} + +check_wallet_migration_with_enter() { + printf "\n" | checkWalletMigration +} + +check_rpc_wallet_with_enter() { + printf "\n" | checkRPCwallet +} + +@test "checkRPCwallet creates the configured descriptor watch-only wallet" { + load_joininbox_bitcoin_functions + + run checkRPCwallet + + [ "$status" -eq 0 ] + [[ "$output" == *"The wallet: watch-only-descriptor-wallet is present and loaded"* ]] + + run wallet_info + [ "$status" -eq 0 ] + [ "$(jq -r '.descriptors' <<<"$output")" = "true" ] + [ "$(jq -r '.private_keys_enabled' <<<"$output")" = "false" ] + run grep -q "walletMigrationDone" "$joinin_conf" + [ "$status" -ne 0 ] +} + +@test "customRPC uses the descriptor wallet RPC endpoint" { + bitcoin-cli \ + -regtest \ + -datadir="$bitcoin_datadir" \ + -rpcuser="$rpc_user" \ + -rpcpassword="$rpc_pass" \ + -rpcport="$rpc_port" \ + -named createwallet \ + wallet_name=watch-only-descriptor-wallet \ + descriptors=true \ + disable_private_keys=true >/dev/null + + load_joininbox_bitcoin_functions + + run customRPC "# Wallet info" "getwalletinfo" "" + + [ "$status" -eq 0 ] + [[ "$output" == *'"walletname": "watch-only-descriptor-wallet"'* ]] + [[ "$output" == *'"descriptors": true'* ]] + [[ "$output" == *'"private_keys_enabled": false'* ]] +} + +@test "checkRPCwallet migrates a persisted wallet.dat configuration on Bitcoin Core v30 or later" { + bitcoin-cli \ + -regtest \ + -datadir="$bitcoin_datadir" \ + -rpcuser="$rpc_user" \ + -rpcpassword="$rpc_pass" \ + -rpcport="$rpc_port" \ + -named createwallet \ + wallet_name=wallet.dat \ + descriptors=true \ + disable_private_keys=true >/dev/null + sed \ + "s/^rpc_wallet_file =.*/rpc_wallet_file = wallet.dat/" \ + "$joinmarket_cfg" >"${joinmarket_cfg}.legacy" + mv "${joinmarket_cfg}.legacy" "$joinmarket_cfg" + + load_joininbox_bitcoin_functions + + run check_rpc_wallet_with_enter + + [ "$status" -eq 0 ] + [[ "$output" == *"Migrating the configured Bitcoin Core wallet"* ]] + [[ "$output" == *"WALLET MIGRATION NOTICE"* ]] + grep -q "^rpc_wallet_file = watch-only-descriptor-wallet$" "$joinmarket_cfg" + grep -q "^walletMigrationDone=true$" "$joinin_conf" + + run wallet_info + [ "$status" -eq 0 ] + [ "$(jq -r '.descriptors' <<<"$output")" = "true" ] + [ "$(jq -r '.private_keys_enabled' <<<"$output")" = "false" ] +} + +@test "migrateLegacyRPCWalletConfig keeps wallet.dat on Bitcoin Core v29.2" { + sed \ + "s/^rpc_wallet_file =.*/rpc_wallet_file = wallet.dat/" \ + "$joinmarket_cfg" >"${joinmarket_cfg}.legacy" + mv "${joinmarket_cfg}.legacy" "$joinmarket_cfg" + + load_joininbox_bitcoin_functions + getConnectedBitcoinCoreVersion() { + echo 290200 + } + getRPC >/dev/null + + run migrateLegacyRPCWalletConfig + + [ "$status" -eq 0 ] + [[ "$output" == *"v29.x or earlier; keeping wallet.dat"* ]] + grep -q "^rpc_wallet_file = wallet.dat$" "$joinmarket_cfg" +} + +@test "checkWalletMigration shows the notice once when wallet.dat exists" { + bitcoin-cli \ + -regtest \ + -datadir="$bitcoin_datadir" \ + -rpcuser="$rpc_user" \ + -rpcpassword="$rpc_pass" \ + -rpcport="$rpc_port" \ + -named createwallet \ + wallet_name=wallet.dat \ + descriptors=true \ + disable_private_keys=true >/dev/null + + load_joininbox_bitcoin_functions + # shellcheck disable=SC2034 + rpc_host="127.0.0.1" + # shellcheck disable=SC2034 + rpc_wallet="watch-only-descriptor-wallet" + + run check_wallet_migration_with_enter + + [ "$status" -eq 0 ] + [[ "$output" == *"WALLET MIGRATION NOTICE"* ]] + grep -q "^walletMigrationDone=true$" "$joinin_conf" + + run checkWalletMigration + [ "$status" -eq 0 ] + [ "$output" = "" ] +} diff --git a/test/run-bats-local.sh b/test/run-bats-local.sh new file mode 100755 index 0000000..ee7320b --- /dev/null +++ b/test/run-bats-local.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +if ! command -v bats >/dev/null 2>&1; then + echo "bats is required. Install bats-core, then rerun this script." >&2 + exit 127 +fi + +bats test/bats