From 7fd7c28bd2df55541bba36221d6ed8fdeb97bd38 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 3 Apr 2023 17:52:43 +0200 Subject: [PATCH 01/56] update bitcoin-scripts to latest, shellcheck fmt (#120) --- scripts/menu.tools.sh | 311 ++++++++++++++++++++++-------------------- 1 file changed, 163 insertions(+), 148 deletions(-) diff --git a/scripts/menu.tools.sh b/scripts/menu.tools.sh index 42196ae..c159df8 100755 --- a/scripts/menu.tools.sh +++ b/scripts/menu.tools.sh @@ -4,30 +4,30 @@ source /home/joinmarket/joinin.conf source /home/joinmarket/_functions.sh function installBitcoinScripts { - if [ ! -d "/home/bitcoin/bitcoin-scripts" ];then + if [ ! -d "/home/bitcoin/bitcoin-scripts" ]; then cd /home/bitcoin/ || exit 1 sudo -u bitcoin git clone https://github.com/kristapsk/bitcoin-scripts.git cd bitcoin-scripts || exit 1 # from https://github.com/kristapsk/bitcoin-scripts/commits/master - sudo -u bitcoin git checkout 9919d7410106aa99e64b605b4c7e836fe629d010 - sudo -u bitcoin chmod +x *.sh + sudo -u bitcoin git checkout 435dc29f377f0563c3b94390fcfe3ba5df3b6f11 + sudo -u bitcoin chmod +x ./*.sh fi } function listCJcandidateTXNs { -clear -cjTXNs=$(grep "Found Joinmarket coinjoin transaction" < \ - /home/joinmarket/.joinmarket/candidates.txt| awk '{print $5}') -installBitcoinScripts -getRPC -for i in $cjTXNs; do - sudo -u bitcoin /home/bitcoin/bitcoin-scripts/checktransaction.sh \ - -rpcwallet=$rpc_wallet $i -done + clear + cjTXNs=$(grep "Found Joinmarket coinjoin transaction" < \ + /home/joinmarket/.joinmarket/candidates.txt | awk '{print $5}') + installBitcoinScripts + getRPC + for i in $cjTXNs; do + sudo -u bitcoin /home/bitcoin/bitcoin-scripts/checktransaction.sh \ + -rpcwallet=$rpc_wallet $i + done } function installBoltzmann { - if [ ! -f "/home/joinmarket/boltzmann/bvenv/bin/activate" ] ; then + if [ ! -f "/home/joinmarket/boltzmann/bvenv/bin/activate" ]; then cd /home/joinmarket/ || exit 1 git clone https://code.samourai.io/oxt/boltzmann.git cd boltzmann || exit 1 @@ -45,9 +45,9 @@ function dialog_inputbox { trap 'rm -f "$inputdata"' EXIT inputdata=$(mktemp -p /dev/shm/) dialog --backtitle "$title" \ - --title "$title" \ - --inputbox " - $text" $height $width 2> "$inputdata" + --title "$title" \ + --inputbox " + $text" $height $width 2>"$inputdata" openMenuIfCancelled $? dialog_output=$(cat $inputdata) } @@ -56,13 +56,13 @@ function getQRstring { trap 'rm -f "$QRstring"' EXIT QRstring=$(mktemp -p /dev/shm/) dialog --backtitle "Display a QR code from any text" \ - --title "Enter any text" \ - --inputbox " - Enter the text to be shown as a QR code" 9 71 2> "$QRstring" + --title "Enter any text" \ + --inputbox " + Enter the text to be shown as a QR code" 9 71 2>"$QRstring" openMenuIfCancelled $? } -isLocalBitcoinCLI=$(sudo -u bitcoin bitcoin-cli -version|grep -c "Bitcoin Core RPC client") +isLocalBitcoinCLI=$(sudo -u bitcoin bitcoin-cli -version | grep -c "Bitcoin Core RPC client") isTxindex=$(sudo -u bitcoin cat /home/bitcoin/.bitcoin/bitcoin.conf | grep -c "txindex=1") # BASIC MENU INFO @@ -78,29 +78,32 @@ BACKTITLE="JoininBox GUI" if [ "${runningEnv}" = standalone ]; then OPTIONS+=( SPECTER "Specter Desktop options") - HEIGHT=$((HEIGHT+1)) - CHOICE_HEIGHT=$((CHOICE_HEIGHT+1)) + HEIGHT=$((HEIGHT + 1)) + CHOICE_HEIGHT=$((CHOICE_HEIGHT + 1)) fi OPTIONS+=( - QR "Display a QR code from any text" - CUSTOMRPC "Run a custom bitcoin RPC with curl" - CJFINDER "Scan blocks for JoinMarket coinjoins") -if [ "$isLocalBitcoinCLI" -gt 0 ] && [ "$isTxindex" -gt 0 ];then + QR "Display a QR code from any text" + CUSTOMRPC "Run a custom bitcoin RPC with curl" + CJFINDER "Scan blocks for JoinMarket coinjoins") +if [ "$isLocalBitcoinCLI" -gt 0 ] && [ "$isTxindex" -gt 0 ]; then OPTIONS+=( CHECKTXN "CLI transaction explorer") - HEIGHT=$((HEIGHT+1)); CHOICE_HEIGHT=$((CHOICE_HEIGHT+1)) - if [ -f /home/joinmarket/.joinmarket/candidates.txt ];then - OPTIONS+=( - LISTCJS "List filtered coinjoin transactions") - HEIGHT=$((HEIGHT+1)); CHOICE_HEIGHT=$((CHOICE_HEIGHT+1)) + HEIGHT=$((HEIGHT + 1)) + CHOICE_HEIGHT=$((CHOICE_HEIGHT + 1)) + if [ -f /home/joinmarket/.joinmarket/candidates.txt ]; then + OPTIONS+=( + LISTCJS "List filtered coinjoin transactions") + HEIGHT=$((HEIGHT + 1)) + CHOICE_HEIGHT=$((CHOICE_HEIGHT + 1)) fi fi - OPTIONS+=( - BOLTZMANN "Analyze the entropy of a transaction") +OPTIONS+=( + BOLTZMANN "Analyze the entropy of a transaction") if [ "${runningEnv}" != mynode ]; then OPTIONS+=( PASSWORD "Change the ssh password") - HEIGHT=$((HEIGHT+1)); CHOICE_HEIGHT=$((CHOICE_HEIGHT+1)) + HEIGHT=$((HEIGHT + 1)) + CHOICE_HEIGHT=$((CHOICE_HEIGHT + 1)) fi if [ "${runningEnv}" != standalone ]; then if grep -Eq "^joinmarketSSH=off" /home/joinmarket/joinin.conf; then @@ -110,132 +113,144 @@ if [ "${runningEnv}" != standalone ]; then fi OPTIONS+=( SSH "$sshAction ssh access with the joinmarket user") - HEIGHT=$((HEIGHT+1)); CHOICE_HEIGHT=$((CHOICE_HEIGHT+1)) + HEIGHT=$((HEIGHT + 1)) + CHOICE_HEIGHT=$((CHOICE_HEIGHT + 1)) fi OPTIONS+=( - LOGS "Show the bitcoind logs on $network" - API "Start the jmwalletd.py as a systemd service" - FULLYNODED "Connect Fully Noded with a QRcode") + LOGS "Show the bitcoind logs on $network" + API "Start the jmwalletd.py as a systemd service" + FULLYNODED "Connect Fully Noded with a QRcode") CHOICE=$(dialog \ - --clear \ - --backtitle "$BACKTITLE" \ - --title "$TITLE" \ - --ok-label "Select" \ - --cancel-label "Back" \ - --menu "$MENU" \ - $HEIGHT $WIDTH $CHOICE_HEIGHT \ - "${OPTIONS[@]}" \ - 2>&1 >/dev/tty) + --clear \ + --backtitle "$BACKTITLE" \ + --title "$TITLE" \ + --ok-label "Select" \ + --cancel-label "Back" \ + --menu "$MENU" \ + $HEIGHT $WIDTH $CHOICE_HEIGHT \ + "${OPTIONS[@]}" \ + 2>&1 >/dev/tty) case $CHOICE in - QR) - getQRstring - datastring=$(cat $QRstring) - clear - echo - echo "Displaying the text:" - echo "$datastring" - echo - if [ ${#datastring} -eq 0 ]; then - echo "# Error='missing string'" - fi - qrencode -t ANSIUTF8 "${datastring}" - echo "(To shrink QR code: MacOS press CMD- / Linux press CTRL-)" - echo - echo "Press ENTER to return to the menu..." - read key;; - CUSTOMRPC) - echo "***DANGER ZONE***" - echo "# See the options at https://developer.bitcoin.org/reference/rpc/" - echo - echo "# Input which method (command) to use" - read method - echo "# Input the parameter(s) (optional)" - read params - customRPC "# custom RPC" "$method" "$params" - echo - echo "Press ENTER to return to the menu..." - read key;; - SPECTER) - /home/joinmarket/standalone/menu.specter.sh;; - PASSWORD) - sudo /home/joinmarket/set.password.sh;; - LOGS) - showBitcoinLogs;; - CHECKTXN) - dialog_inputbox "CLI transaction explorer" "\nUsing: https://github.com/kristapsk/bitcoin-scripts\n\nPaste a TXID to check" 11 71 - clear - installBitcoinScripts - getRPC - echo - echo "# Running the command:" - echo "sudo -u bitcoin /home/bitcoin/bitcoin-scripts/checktransaction.sh -rpcwallet=$rpc_wallet $dialog_output" - echo - sudo -u bitcoin /home/bitcoin/bitcoin-scripts/checktransaction.sh -rpcwallet=$rpc_wallet $dialog_output - echo - echo "Press ENTER to return to the menu..." - read key;; - BOLTZMANN) - dialog_inputbox "Boltzmann transaction entropy analysis" "\nUsing: https://code.samourai.io/oxt/boltzmann\n\nPaste a TXID to analyze" 11 71 - clear - installBoltzmann - python /home/joinmarket/start.boltzmann.py --txid=$dialog_output - echo - echo "Press ENTER to return to the menu..." - read key;; - CJFINDER) - BLOCKHEIGHT=$(customRPC "" "getblockchaininfo" ""\ - |grep blocks|awk '{print $2}'|cut -d, -f1) - dialog_inputbox "snicker-finder.py" \ +QR) + getQRstring + datastring=$(cat $QRstring) + clear + echo + echo "Displaying the text:" + echo "$datastring" + echo + if [ ${#datastring} -eq 0 ]; then + echo "# Error='missing string'" + fi + qrencode -t ANSIUTF8 "${datastring}" + echo "(To shrink QR code: MacOS press CMD- / Linux press CTRL-)" + echo + echo "Press ENTER to return to the menu..." + read key + ;; +CUSTOMRPC) + echo "***DANGER ZONE***" + echo "# See the options at https://developer.bitcoin.org/reference/rpc/" + echo + echo "# Input which method (command) to use" + read method + echo "# Input the parameter(s) (optional)" + read params + customRPC "# custom RPC" "$method" "$params" + echo + echo "Press ENTER to return to the menu..." + read key + ;; +SPECTER) + /home/joinmarket/standalone/menu.specter.sh + ;; +PASSWORD) + sudo /home/joinmarket/set.password.sh + ;; +LOGS) + showBitcoinLogs + ;; +CHECKTXN) + dialog_inputbox "CLI transaction explorer" "\nUsing: https://github.com/kristapsk/bitcoin-scripts\n\nPaste a TXID to check" 11 71 + clear + installBitcoinScripts + getRPC + echo + echo "# Running the command:" + echo "sudo -u bitcoin /home/bitcoin/bitcoin-scripts/checktransaction.sh -rpcwallet=$rpc_wallet $dialog_output" + echo + sudo -u bitcoin /home/bitcoin/bitcoin-scripts/checktransaction.sh -rpcwallet=$rpc_wallet $dialog_output + echo + echo "Press ENTER to return to the menu..." + read key + ;; +BOLTZMANN) + dialog_inputbox "Boltzmann transaction entropy analysis" "\nUsing: https://code.samourai.io/oxt/boltzmann\n\nPaste a TXID to analyze" 11 71 + clear + installBoltzmann + python /home/joinmarket/start.boltzmann.py --txid=$dialog_output + echo + echo "Press ENTER to return to the menu..." + read key + ;; +CJFINDER) + BLOCKHEIGHT=$(customRPC "" "getblockchaininfo" "" | + grep blocks | awk '{print $2}' | cut -d, -f1) + dialog_inputbox "snicker-finder.py" \ "\nUsing: https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/\ master/scripts/snicker/snicker-finder.py\n\n\ Note that scanning the blocks is slow.\n\ Current blockheight is: $BLOCKHEIGHT\n\ Input how many previous blocks from the tip you want to scan" 14 108 - clear - echo - echo "Running the command:" - echo "python snicker/snicker-finder.py -j $((BLOCKHEIGHT - dialog_output)) \ + clear + echo + echo "Running the command:" + echo "python snicker/snicker-finder.py -j $((BLOCKHEIGHT - dialog_output)) \ -f /home/joinmarket/.joinmarket/candidates.txt" + echo + python /home/joinmarket/joinmarket-clientserver/scripts/snicker/snicker-finder.py -j $((BLOCKHEIGHT - dialog_output)) -f /home/joinmarket/.joinmarket/candidates.txt + echo + echo "The transaction details are saved in /home/joinmarket/.joinmarket/candidates.txt" + echo "To display the file in the terminal use:" + echo "'cat /home/joinmarket/.joinmarket/candidates.txt'" + if [ $isLocalBitcoinCLI -gt 0 ]; then + echo "or menu -> TOOLS -> CHECKTXN for a CLI transaction explorer" echo - python /home/joinmarket/joinmarket-clientserver/scripts/snicker/snicker-finder.py\ - -j $((BLOCKHEIGHT - dialog_output)) -f /home/joinmarket/.joinmarket/candidates.txt - echo - echo "The transaction details are saved in /home/joinmarket/.joinmarket/candidates.txt" - echo "To display the file in the terminal use:" - echo "'cat /home/joinmarket/.joinmarket/candidates.txt'" - if [ $isLocalBitcoinCLI -gt 0 ];then - echo "or menu -> TOOLS -> CHECKTXN for a CLI transaction explorer" - echo - echo "Press ENTER for an overview of the JoinMarket coinjoins found with CHECKTXN" - read key - listCJcandidateTXNs - fi - echo "Press ENTER to return to the menu..." - read key;; - LISTCJS) + echo "Press ENTER for an overview of the JoinMarket coinjoins found with CHECKTXN" + read key listCJcandidateTXNs - echo "Press ENTER to return to the menu..." - read key;; - SSH) - if [ $sshAction = "Disable" ];then - sudo /home/joinmarket/set.ssh.sh off - elif [ $sshAction = "Enable" ];then - sudo /home/joinmarket/set.ssh.sh on - fi - echo - echo "Press ENTER to return to the menu..." - read key;; - API) - clear - echo - /home/joinmarket/install.joinmarket-api.sh on - echo "Press ENTER to return to the menu..." - read key;; - FULLYNODED) - clear - /home/joinmarket/install.joinmarket-api.sh connect - echo "Press ENTER to return to the menu..." - read key;; + fi + echo "Press ENTER to return to the menu..." + read key + ;; +LISTCJS) + listCJcandidateTXNs + echo "Press ENTER to return to the menu..." + read key + ;; +SSH) + if [ $sshAction = "Disable" ]; then + sudo /home/joinmarket/set.ssh.sh off + elif [ $sshAction = "Enable" ]; then + sudo /home/joinmarket/set.ssh.sh on + fi + echo + echo "Press ENTER to return to the menu..." + read key + ;; +API) + clear + echo + /home/joinmarket/install.joinmarket-api.sh on + echo "Press ENTER to return to the menu..." + read key + ;; +FULLYNODED) + clear + /home/joinmarket/install.joinmarket-api.sh connect + echo "Press ENTER to return to the menu..." + read key + ;; esac From b07faa68e7d90e2c50b705587cc5c260bb9a4238 Mon Sep 17 00:00:00 2001 From: openoms Date: Sun, 16 Apr 2023 08:21:33 +0100 Subject: [PATCH 02/56] feat: add custom change address option --- scripts/menu.send.sh | 64 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/scripts/menu.send.sh b/scripts/menu.send.sh index d4bc5c7..8ea0a8b 100755 --- a/scripts/menu.send.sh +++ b/scripts/menu.send.sh @@ -21,8 +21,10 @@ coinjoin: $makercountMessage miner fee: $txfeeMessage destination address: $(cat "$address") +change address (optional): +$changeAddressMessage -Enter a number between 0 to 4 to choose the mixdepth" 17 69 2> "$mixdepth" +Enter a number between 0 to 4 to choose the mixdepth" 19 69 2> "$mixdepth" openMenuIfCancelled $? # amount @@ -38,9 +40,11 @@ coinjoin: $makercountMessage miner fee: $txfeeMessage destination address: $(cat "$address") +change address (optional): +$changeAddressMessage Enter the amount to send in satoshis -Use 0 to sweep the mixdepth without a change output" 18 69 2> "$amount" +Use 0 to sweep the mixdepth without a change output" 20 69 2> "$amount" openMenuIfCancelled $? amountsats="$(cat "$amount") sats" @@ -57,10 +61,12 @@ coinjoin: $makercountMessage miner fee: $txfeeMessage destination address: $(cat "$address") +change address (optional): +$changeAddressMessage Enter the number of makers to coinjoin with (min 4) Leave empty for the default 5-9 (randomized) -Enter 0 to send without a coinjoin." 19 69 2> "$makercount" +Enter 0 to send without a coinjoin." 21 69 2> "$makercount" openMenuIfCancelled $? varMakercount=$(cat "$makercount") if [ ${#varMakercount} -eq 0 ]; then @@ -74,7 +80,6 @@ else makercountOption="-N $varMakercount" fi - # txfee trap 'rm -f "$txfee"' EXIT txfee=$(mktemp -p /dev/shm/) @@ -88,9 +93,11 @@ coinjoin: $makercountMessage miner fee: $txfeeMessage destination address: $(cat "$address") +change address (optional): +$changeAddressMessage Enter the miner fee to be used for the transaction in sat/byte -Leave empty to use the default fee (set in the joinmarket.cfg)" 18 69 2> "$txfee" +Leave empty to use the default fee (set in the joinmarket.cfg)" 20 69 2> "$txfee" openMenuIfCancelled $? varTxfee=$(cat "$txfee") if [ ${#varTxfee} -eq 0 ]; then @@ -119,10 +126,45 @@ coinjoin: $makercountMessage miner fee: $txfeeMessage destination address: $(cat "$address") +change address (optional): +$changeAddressMessage -Paste the destination address" 17 69 2> "$address" +Paste the destination address" 19 69 2> "$address" openMenuIfCancelled $? +# changeAddress +trap 'rm -f "$changeAdress"' EXIT +changeAddress=$(mktemp -p /dev/shm/) +if [ "$amountsats" != "0 sats" ]; then + dialog --backtitle "Custom change address" \ + --title "Custom change address" \ + --inputbox " +From the wallet: $walletFileName +mixdepth: $(cat "$mixdepth") +send: $amountsats +coinjoin: $makercountMessage +miner fee: $txfeeMessage +destination address: +$(cat "$address") +change address (optional): +$changeAddressMessage + +Paste the address to receive the change to +or leave empty to use an internal address in the mixdepth$(cat $mixdepth)" 20 69 2> "$changeAddress" +openMenuIfCancelled $? +fi +varChangeAddress=$(cat "$changeAddress") +if [ "$amountsats" = "0 sats" ]; then + changeAddressMessage="none" + changeAddressOption="" +elif [ ${#varChangeAddress} -eq 0 ]; then + changeAddressMessage="internal address in m$(cat $mixdepth)" + changeAddressOption="" +else + changeAddressMessage="$varChangeAddress" + changeAddressOption="--custom-change $varChangeAddress" +fi + if [ "${RPCoverTor}" = "on" ]; then tor="torsocks" else @@ -139,7 +181,9 @@ send: $amountsats coinjoin: $makercountMessage miner fee: $txfeeMessage destination address: -$(cat "$address")" 15 69 +$(cat "$address") +change address (optional): +$changeAddressMessage" 17 69 # make decison pressed=$? @@ -150,12 +194,12 @@ case $pressed in echo "Running the command: $tor python sendpayment.py \ -m $(cat "$mixdepth") $makercountOption $(sed "s#$walletPath##g" < "$wallet" ) \ -$(cat "$amount") $(cat "$address") $txfeeOption +$(cat "$amount") $(cat "$address") $txfeeOption $changeAddressOption " # run $tor python ~/joinmarket-clientserver/scripts/sendpayment.py \ -m "$(cat "$mixdepth")" $makercountOption "$(cat "$wallet")" \ - "$(cat "$amount")" "$(cat "$address")" $txfeeOption + "$(cat "$amount")" "$(cat "$address")" $txfeeOption $changeAddressOption ;; 1) echo "Cancelled" @@ -165,4 +209,4 @@ $(cat "$amount") $(cat "$address") $txfeeOption echo "ESC pressed." exit 1 ;; -esac \ No newline at end of file +esac From e662759a2b9391a9d78db230f6ccef44cc4e17c4 Mon Sep 17 00:00:00 2001 From: openoms Date: Sun, 16 Apr 2023 08:21:44 +0100 Subject: [PATCH 03/56] chore: remove setIRCtoTor --- scripts/_functions.sh | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/scripts/_functions.sh b/scripts/_functions.sh index 53802b5..289e536 100755 --- a/scripts/_functions.sh +++ b/scripts/_functions.sh @@ -230,36 +230,6 @@ function updateJoininBox() { copyJoininboxScripts } -function setIRCtoTor() { - if [ "${runBehindTor}" = "on" ]; then - # darkscience - sed -i "s/^host = irc.darkscience.net/#host = irc.darkscience.net/g" $JMcfgPath - sed -i "s/^#host = dark.*/host = darkirc6tqgpnwd3blln3yfv5ckl47eg7llfxkmtovrv7c7iwohhb6ad.onion/g" $JMcfgPath - # hackint - sed -i "s/^host = irc.hackint.org/#host = irc.hackint.org/g" $JMcfgPath - sed -i "s/^#host = ncwkrwxpq2ikcngxq3dy2xctuheniggtqeibvgofixpzvrwpa77tozqd.onion/host = ncwkrwxpq2ikcngxq3dy2xctuheniggtqeibvgofixpzvrwpa77tozqd.onion/g" $JMcfgPath - # anarplex - sed -i "s/^host = agora.anarplex.net/#host = agora.anarplex.net/g" $JMcfgPath - sed -i "s/^#host = vxecvd6lc4giwtasjhgbrr3eop6pzq6i5rveracktioneunalgqlwfad.onion/host = vxecvd6lc4giwtasjhgbrr3eop6pzq6i5rveracktioneunalgqlwfad.onion/g" $JMcfgPath - # ilita - sed -i "s/^#host = ilitafrzzgxymv6umx2ux7kbz3imyeko6cnqkvy4nisjjj4qpqkrptid.onion/host = ilitafrzzgxymv6umx2ux7kbz3imyeko6cnqkvy4nisjjj4qpqkrptid.onion/g" $JMcfgPath - - # socks5 - sed -i "s/^socks5 = false/#socks5 = false/g" $JMcfgPath - sed -i "s/^#socks5 = true/socks5 = true/g" $JMcfgPath - sed -i "s/^#socks5_host = localhost/socks5_host = localhost/g" $JMcfgPath - sed -i "s/^#socks5_port = 9050/socks5_port = 9050/g" $JMcfgPath - # port - sed -i "s/^#port = 6667/port = 6667/g" $JMcfgPath - # usessl - sed -i "s/^#usessl = false/usessl = false/g" $JMcfgPath - - echo "# Edited the joinmarket.cfg to connect to IRC servers over Tor only." - else - echo "# Tor is not active, will communicate with IRC servers via clearnet" - fi -} - function generateJMconfig() { if [ ! -f "$JMcfgPath" ] ; then echo "# Generating joinmarket.cfg with default settings" @@ -271,11 +241,7 @@ function generateJMconfig() { echo "# The joinmarket.cfg is present" echo fi - if [ $(echo $currentJMversion | cut -d'v' -f2 | awk -F'.' '{print $1$2$3}' ) -lt 96 ];then - setIRCtoTor - else - echo "# Since JoinMarket v0.9.6 the Tor connection is default for IRC servers." - fi + # set strict permission to joinmarket.cfg sudo chmod 600 $JMcfgPath || exit 1 From 6667f9b99e1ba423a297e5c6110a49e85d77f766 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 17 Apr 2023 13:11:13 +0100 Subject: [PATCH 04/56] jam update to v0.1.5, nodejs update, fmt (#122) --- scripts/install.joinmarket-api.sh | 10 +++++----- scripts/jam-remote/bonus.nodejs.sh | 16 ++++++++-------- scripts/jam-remote/install.jam.sh | 9 +++------ 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/scripts/install.joinmarket-api.sh b/scripts/install.joinmarket-api.sh index 1fcb83c..f98c62a 100644 --- a/scripts/install.joinmarket-api.sh +++ b/scripts/install.joinmarket-api.sh @@ -3,7 +3,7 @@ USERNAME=joinmarket HOME_DIR=/home/$USERNAME -if [ ${#1} -eq 0 ]||[ "$1" = "-h" ]||[ "$1" = "--help" ];then +if [ ${#1} -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then echo "Script to start/stop the joinmarket-api.service." echo "Usage:" echo "start.joinmarket-api.sh [on|off]" @@ -64,7 +64,7 @@ elif [ "$1" = connect ]; then base64cert=$(base64 -w 0 ${HOME_DIR}/.joinmarket/ssl/cert.pem) url="http://${torAddress}:28183?cert=${base64cert}" - if [ "$runningEnv" = raspiblitz ];then + if [ "$runningEnv" = raspiblitz ]; then sudo /home/admin/config.scripts/blitz.display.sh qr "${url}" fi echo @@ -80,14 +80,14 @@ elif [ "$1" = connect ]; then echo "If the Qrcode is still too large shrink the letters by pressing the keys:" echo "Ctrl and Minus (or Cmd and Minus if you are on a Mac)" echo - if [ $runningEnv = raspiblitz ];then + if [ $runningEnv = raspiblitz ]; then echo "# Press enter to hide the QRcode from the LCD" read key sudo /home/admin/config.scripts/blitz.display.sh hide fi exit 0 -elif [ "$1" = off ];then +elif [ "$1" = off ]; then # remove hidden service $HOME_DIR/install.hiddenservice.sh off joinmarket-api @@ -95,4 +95,4 @@ elif [ "$1" = off ];then sudo systemctl stop joinmarket-api sudo systemctl disable joinmarket-api sudo rm -f /etc/systemd/system/joinmarket-api.service -fi \ No newline at end of file +fi diff --git a/scripts/jam-remote/bonus.nodejs.sh b/scripts/jam-remote/bonus.nodejs.sh index b58f762..33b02c3 100755 --- a/scripts/jam-remote/bonus.nodejs.sh +++ b/scripts/jam-remote/bonus.nodejs.sh @@ -3,26 +3,26 @@ # consider installing with apt when updated next # https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions -VERSION="v16.14.2" +VERSION="v18.12.0" # command info if [ $# -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "-help" ]; then - echo "config script to install NodeJs $VERSION" - echo "bonus.nodejs.sh [on|off|info]" - exit 1 + echo "config script to install NodeJs $VERSION" + echo "bonus.nodejs.sh [on|off|info]" + exit 1 fi - # determine nodeJS VERSION and DISTRO +# determine nodeJS VERSION and DISTRO isARM=$(uname -m | grep -c 'arm') isAARCH64=$(uname -m | grep -c 'aarch64') isX86_64=$(uname -m | grep -c 'x86_64') -if [ ${isARM} -eq 1 ] ; then +if [ ${isARM} -eq 1 ]; then DISTRO="linux-armv7l" CHECKSUM="${CHECKSUM_linux_armv7l}" -elif [ ${isAARCH64} -eq 1 ] ; then +elif [ ${isAARCH64} -eq 1 ]; then DISTRO="linux-arm64" CHECKSUM="${CHECKSUM_linux_arm64}" -elif [ ${isX86_64} -eq 1 ] ; then +elif [ ${isX86_64} -eq 1 ]; then DISTRO="linux-x64" CHECKSUM="${CHECKSUM_linux_x64}" elif [ ${#DISTRO} -eq 0 ]; then diff --git a/scripts/jam-remote/install.jam.sh b/scripts/jam-remote/install.jam.sh index 799adfd..10c67e2 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=0.1.4 +WEBUI_VERSION=0.1.5 REPO=joinmarket-webui/jam HOME_DIR=/home/${USERNAME} APP_DIR=webui @@ -37,7 +37,6 @@ ${fingerprint}\n exit 0 fi - # switch on if [ "$1" = "on" ]; then isInstalled=$(sudo ls $HOME_DIR 2>/dev/null | grep -c "$APP_DIR") @@ -61,7 +60,7 @@ if [ "$1" = "on" ]; then sudo -u $USERNAME git reset --hard v${WEBUI_VERSION} sudo -u $USERNAME bash ${SOURCEDIR}/../verify.git.sh \ - "${PGPsigner}" "${PGPpubkeyLink}" "${PGPpubkeyFingerprint}" "v${WEBUI_VERSION}" || exit 1 + "${PGPsigner}" "${PGPpubkeyLink}" "${PGPpubkeyFingerprint}" "v${WEBUI_VERSION}" || exit 1 cd $HOME_DIR || exit 1 sudo -u $USERNAME mv jam $APP_DIR @@ -94,7 +93,6 @@ if [ "$1" = "on" ]; then exit 0 fi - # update if [ "$1" = "update" ]; then isInstalled=$(sudo ls $HOME_DIR 2>/dev/null | grep -c "$APP_DIR") @@ -126,7 +124,7 @@ if [ "$1" = "update" ]; then sudo -u $USERNAME git reset --hard v${version} sudo -u $USERNAME bash ${SOURCEDIR}/../verify.git.sh \ - "${PGPsigner}" "${PGPpubkeyLink}" "${PGPpubkeyFingerprint}" "v${version}" || exit 1 + "${PGPsigner}" "${PGPpubkeyLink}" "${PGPpubkeyFingerprint}" "v${version}" || exit 1 cd $HOME_DIR || exit 1 sudo -u $USERNAME mv jam $APP_DIR @@ -147,7 +145,6 @@ if [ "$1" = "update" ]; then exit 0 fi - # switch off if [ "$1" = "off" ]; then echo "*** UNINSTALL JAM ***" From fd39fade3525677755ee548c97915dbe251032fc Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 7 May 2023 09:18:20 +0100 Subject: [PATCH 05/56] fix: pin werkzeug dependency to 2.2.0 + fmt (#124) --- scripts/install.joinmarket.sh | 87 ++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/scripts/install.joinmarket.sh b/scripts/install.joinmarket.sh index 9668391..747cc69 100755 --- a/scripts/install.joinmarket.sh +++ b/scripts/install.joinmarket.sh @@ -11,7 +11,7 @@ nocolor="\033[0m" red="\033[31m" ## see https://github.com/rootzoll/raspiblitz/blob/v1.7/build_sdcard.sh for code comments -usage(){ +usage() { printf %s"${me} [--option ] a script to install, update or configure JoinMarket @@ -35,28 +35,31 @@ if [ $# -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then usage fi -error_msg(){ printf %s"${red}${me}: ${1}${nocolor}\n"; exit 1; } -assign_value(){ +error_msg() { + printf %s"${red}${me}: ${1}${nocolor}\n" + exit 1 +} +assign_value() { case "${2}" in - --*) value="${2#--}";; - -*) value="${2#-}";; - *) value="${2}" + --*) value="${2#--}" ;; + -*) value="${2#-}" ;; + *) value="${2}" ;; esac case "${value}" in - 0) value="false";; - 1) value="true";; + 0) value="false" ;; + 1) value="true" ;; esac eval "${1}"="\"${value}\"" } -get_arg(){ +get_arg() { case "${3}" in - ""|-*) error_msg "Option '${2}' requires an argument.";; + "" | -*) error_msg "Option '${2}' requires an argument." ;; esac assign_value "${1}" "${3}" } -range_argument(){ +range_argument() { name="${1}" eval var='$'"${1}" shift @@ -71,17 +74,29 @@ range_argument(){ while :; do case "${1}" in - -*=*) opt="${1%=*}"; arg="${1#*=}"; shift_n=1;; - -*) opt="${1}"; arg="${2}"; shift_n=2;; - *) opt="${1}"; arg="${2}"; shift_n=1;; + -*=*) + opt="${1%=*}" + arg="${1#*=}" + shift_n=1 + ;; + -*) + opt="${1}" + arg="${2}" + shift_n=2 + ;; + *) + opt="${1}" + arg="${2}" + shift_n=1 + ;; esac case "${opt}" in - -i|-i=*|--install|--install=*) get_arg install "${opt}" "${arg}";; - -v|-v=*|--version|--version=*) get_arg version "${opt}" "${arg}";; - -q|-q=*|--qtgui|--qtgui=*) get_arg qtgui "${opt}" "${arg}";; - -u|-u=*|--user|--user=*) get_arg user "${opt}" "${arg}";; - "") break;; - *) error_msg "Invalid option: ${opt}";; + -i | -i=* | --install | --install=*) get_arg install "${opt}" "${arg}" ;; + -v | -v=* | --version | --version=*) get_arg version "${opt}" "${arg}" ;; + -q | -q=* | --qtgui | --qtgui=*) get_arg qtgui "${opt}" "${arg}" ;; + -u | -u=* | --user | --user=*) get_arg user "${opt}" "${arg}" ;; + "") break ;; + *) error_msg "Invalid option: ${opt}" ;; esac shift "${shift_n}" done @@ -110,7 +125,7 @@ if [ "${user}" != "joinmarket" ]; then # configure for usage without password entry checkEntry=$(sudo cat /etc/sudoers | grep -c "${user} ALL=(ALL) NOPASSWD:ALL") if [ ${checkEntry} -eq 0 ]; then - echo "${user} ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers + echo "${user} ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers fi # Autostart checkEntry=$(sudo -u ${user} cat /home/${user}/.bashrc | grep -c "jmvenv/bin/activate") @@ -142,7 +157,7 @@ function installJoinMarket() { # PySide2 for armf: https://packages.debian.org/buster/python3-pyside2.qtcore echo "# Installing ARM specific dependencies to run the QT GUI" sudo apt-get install -y python3-pyside2.qtcore python3-pyside2.qtgui \ - python3-pyside2.qtwidgets zlib1g-dev libjpeg-dev python3-pyqt5 libltdl-dev + python3-pyside2.qtwidgets zlib1g-dev libjpeg-dev python3-pyqt5 libltdl-dev fi # https://github.com/JoinMarket-Org/joinmarket-clientserver/issues/668#issuecomment-717815719 sudo apt-get install -y build-essential automake pkg-config libffi-dev python3-dev @@ -205,14 +220,14 @@ function installJoinMarket() { # Use specific python version, if set python_args="" - if [[ ! -z "${JM_PYTHON}" ]]; then + if [[ -n "${JM_PYTHON}" ]]; then python_args="--python=${JM_PYTHON}" fi # do not clear screen during installation sudo -u ${user} sed -i 's/clear//g' install.sh # do not stop at installing Debian dependencies sudo -u ${user} sed -i \ - "s#^ if ! sudo apt-get install \${deb_deps\[@\]}; then#\ + "s#^ if ! sudo apt-get install \${deb_deps\[@\]}; then#\ if ! sudo apt-get install -y \${deb_deps\[@\]}; then#g" install.sh if [ ${cpu} != "x86_64" ]; then @@ -229,6 +244,12 @@ function installJoinMarket() { sudo -u ${user} sed -i "s#PyQt5!=5.15.0,!=5.15.1,!=5.15.2,!=6.0##g" jmqtui/setup.py fi + # pin werkzeug dependency to 2.2.0 as in: + # https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/1485/files + if ! grep 'werkzeug==' jmclient/setup.py; then + sed -i "s/autobahn==20.12.3/&', 'werkzeug==2.2.0/g" jmclient/setup.py + fi + if [ "${qtgui}" = "false" ]; then GUIchoice="--without-qt" else @@ -241,8 +262,10 @@ function installJoinMarket() { else sudo -u ${user} ./install.sh "${GUIchoice}" "$python_args" || exit 1 fi - currentJMversion=$(cd /home/${user}/joinmarket-clientserver 2>/dev/null; \ - git describe --tags 2>/dev/null) + currentJMversion=$( + cd /home/${user}/joinmarket-clientserver 2>/dev/null + git describe --tags 2>/dev/null + ) echo echo "# installed JoinMarket $currentJMversion" echo @@ -256,9 +279,9 @@ if [ "$install" = "config" ]; then fi # show info dialog \ - --title "Configure JoinMarket" \ - --exit-label "Continue to edit the joinmarket.cfg" \ - --textbox "/home/joinmarket/info.conf.txt" 43 108 + --title "Configure JoinMarket" \ + --exit-label "Continue to edit the joinmarket.cfg" \ + --textbox "/home/joinmarket/info.conf.txt" 43 108 # edit joinmarket.cfg /home/${user}/set.conf.sh $JMcfgPath exit 0 @@ -266,7 +289,7 @@ fi if [ "$install" = "install" ]; then # install joinmarket - if [ ! -f "/home/${user}/joinmarket-clientserver/jmvenv/bin/activate" ] ; then + if [ ! -f "/home/${user}/joinmarket-clientserver/jmvenv/bin/activate" ]; then echo echo "# JoinMarket is not yet installed - proceeding now" echo @@ -274,10 +297,10 @@ if [ "$install" = "install" ]; then errorOnInstall $? echo "# Check for optional dependencies: matplotlib and scipy" activateJMvenv - if [ "$(pip list | grep -c matplotlib)" -eq 0 ];then + if [ "$(pip list | grep -c matplotlib)" -eq 0 ]; then pip install matplotlib fi - if [ "$(pip list | grep -c scipy)" -eq 0 ];then + if [ "$(pip list | grep -c scipy)" -eq 0 ]; then # https://stackoverflow.com/questions/7496547/does-python-scipy-need-blas sudo apt-get install -y gfortran libopenblas-dev liblapack-dev # fix 'No space left on device' with 2GB RAM From 83ab1e5b2aa3fadde6ecb8d06767aad2f762697f Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 7 May 2023 12:43:53 +0100 Subject: [PATCH 06/56] ci: update amd64 base image to debian-11.7.0 (#125) --- ci/amd64/debian/joininbox-amd64-debian.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index 46ed554..dc5b675 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -11,8 +11,8 @@ "http_directory": "{{template_dir}}/http", "http_proxy": "{{env `http_proxy`}}", "https_proxy": "{{env `https_proxy`}}", - "iso_checksum": "e482910626b30f9a7de9b0cc142c3d4a079fbfa96110083be1d0b473671ce08d", - "iso_name": "debian-11.6.0-amd64-netinst.iso", + "iso_checksum": "eb3f96fd607e4b67e80f4fc15670feb7d9db5be50f4ca8d0bf07008cb025766b", + "iso_name": "debian-11.7.0-amd64-netinst.iso", "memory": "2048", "mirror": "http://cdimage.debian.org/cdimage/release", "mirror_directory": "current/amd64/iso-cd", From 6032311eca06825fd6cad14dc5ef95fbbd3b3266 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Thu, 15 Jun 2023 20:32:41 +0000 Subject: [PATCH 07/56] add pruned.host4coins.net/blocks + update to debian 12.0.0 (#131) * fix: fix echo-s and format * feat: add pruned.host4coins.net/blocks, fix data owner * fix: check bitcoin install permission * fix: prefer downloads over ipv4 * add hashFileSigName * update to debian-12.0.0-amd64-netinst image * fix the owner of the decompressed blocks * delete old blocks and chainstates --- build_joininbox.sh | 11 +- ci/amd64/debian/joininbox-amd64-debian.json | 4 +- scripts/_commands.sh | 10 +- scripts/_functions.bitcoincore.sh | 102 ++++++++-------- scripts/install.joinmarket.sh | 6 +- scripts/jam-remote/README.md | 2 +- scripts/jam-remote/bonus.nodejs.sh | 4 +- scripts/menu.config.sh | 8 +- scripts/standalone/_functions.standalone.sh | 123 +++++++++++--------- scripts/verify.git.sh | 2 +- 10 files changed, 144 insertions(+), 128 deletions(-) diff --git a/build_joininbox.sh b/build_joininbox.sh index 8cba086..7af37fa 100644 --- a/build_joininbox.sh +++ b/build_joininbox.sh @@ -78,7 +78,7 @@ if [ $(cat /etc/os-release 2>/dev/null | grep -c 'Debian') -gt 0 ]; then elif [ $(cat /etc/os-release 2>/dev/null | grep -c 'Ubuntu') -gt 0 ]; then baseimage="ubuntu" else - echo "\n!!! FAIL: Base Image cannot be detected or is not supported." + echo "\n# FAIL: Base Image cannot be detected or is not supported." cat /etc/os-release 2>/dev/null uname -a exit 1 @@ -274,9 +274,9 @@ if [ "${cpu}" = "armv7l" ] || [ "${cpu}" = "armv6l" ]; then # dependencies apt-get install software-properties-common build-essential libnss3-dev zlib1g-dev libgdbm-dev libncurses5-dev libssl-dev libffi-dev libreadline-dev libsqlite3-dev libbz2-dev -y # download - wget --progress=bar:force https://www.python.org/ftp/python/${pythonVersion}/Python-${pythonVersion}.tgz + wget --prefer-family=ipv4 --progress=bar:force https://www.python.org/ftp/python/${pythonVersion}/Python-${pythonVersion}.tgz # optional signature for verification - wget --progress=bar:force https://www.python.org/ftp/python/${pythonVersion}/Python-${pythonVersion}.tgz.asc + wget --prefer-family=ipv4 --progress=bar:force https://www.python.org/ftp/python/${pythonVersion}/Python-${pythonVersion}.tgz.asc # get PGP pubkey of Ned Deily (Python release signing key) gpg --recv-key 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D # check for: Good signature from "Pablo Galindo Salgado " @@ -322,8 +322,7 @@ else update-alternatives --install /usr/bin/python python /usr/bin/python3.11 1 echo "# python calls python3.11" else - echo "!!! FAIL !!!" - echo "There is no tested version of python present" + echo "# FAIL- there is no tested version of python present" exit 1 fi fi @@ -463,7 +462,7 @@ if [ "$torTest" != "Congratulations. This browser is configured to use Tor." ]; echo "torKeyAvailable=${torKeyAvailable}" if [ ${torKeyAvailable} -eq 0 ]; then # https://support.torproject.org/apt/tor-deb-repo/ - wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --import + wget --prefer-family=ipv4 -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --import gpg --export A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89 | apt-key add - echo "OK" else diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index dc5b675..ae73dd8 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -11,8 +11,8 @@ "http_directory": "{{template_dir}}/http", "http_proxy": "{{env `http_proxy`}}", "https_proxy": "{{env `https_proxy`}}", - "iso_checksum": "eb3f96fd607e4b67e80f4fc15670feb7d9db5be50f4ca8d0bf07008cb025766b", - "iso_name": "debian-11.7.0-amd64-netinst.iso", + "iso_checksum": "3b0e9718e3653435f20d8c2124de6d363a51a1fd7f911b9ca0c6db6b3d30d53e", + "iso_name": "debian-12.0.0-amd64-netinst.iso", "memory": "2048", "mirror": "http://cdimage.debian.org/cdimage/release", "mirror_directory": "current/amd64/iso-cd", diff --git a/scripts/_commands.sh b/scripts/_commands.sh index 08da9b1..fa42c23 100755 --- a/scripts/_commands.sh +++ b/scripts/_commands.sh @@ -1,7 +1,7 @@ #!/bin/bash # source aliases from /home/joinmarket/_aliases.sh -if [ -f /home/joinmarket/_aliases.sh ];then +if [ -f /home/joinmarket/_aliases.sh ]; then source /home/joinmarket/_aliases.sh fi @@ -14,7 +14,7 @@ function menu() { } # command: newnym -function newnym(){ +function newnym() { if [ "$(cat /home/joinmarket/joinin.conf 2>/dev/null | grep -c "runBehindTor=on")" -eq 1 ]; then echo "# Changing Tor circuits..." echo "# Savind old ID..." @@ -26,11 +26,11 @@ function newnym(){ newID=$(curl --connect-timeout 15 --socks5-hostname 127.0.0.1:9050 ifconfig.me 2>/dev/null) echo if [ ${oldID} = ${newID} ]; then - echo "# !!! FAIL: Identity did not change. Read error message above." + echo "# FAIL: Identity did not change. Read error message above." echo "# Exiting for precaution." exit 0 else - echo "# !!! SUCCESS" + echo "# SUCCESS" echo "# Old id: " ${oldID} echo "# New id: " ${newID} fi @@ -41,7 +41,7 @@ function newnym(){ # command: torthistx function torthistx() { - if [ "$(cat /home/joinmarket/joinin.conf 2>/dev/null | grep -c "runBehindTor=on")" -eq 1 ];then + if [ "$(cat /home/joinmarket/joinin.conf 2>/dev/null | grep -c "runBehindTor=on")" -eq 1 ]; then echo if [ "$(cat /home/joinmarket/joinin.conf 2>/dev/null | grep -c "network=signet")" -eq 1 ]; then newnym diff --git a/scripts/_functions.bitcoincore.sh b/scripts/_functions.bitcoincore.sh index b7dfa80..f9f415c 100755 --- a/scripts/_functions.bitcoincore.sh +++ b/scripts/_functions.bitcoincore.sh @@ -24,18 +24,18 @@ function downloadBitcoinCore() { # receive signer key if ! gpg -k | grep "${PGPpubkey}"; then if ! gpg --keyserver hkp://keyserver.ubuntu.com --recv-key "${PGPpubkey}"; then - echo "# !!! FAIL !!! Couldn't download ${PGPname}'s PGP pubkey" + echo "# FAIL - Couldn't download ${PGPname}'s PGP pubkey" exit 1 fi fi # download signed binary sha256 hash sum file if [ ! -f SHA256SUMS ]; then - sudo -u joinmarket wget --progress=bar:force https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/SHA256SUMS + sudo -u joinmarket wget --prefer-family=ipv4 --progress=bar:force https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/SHA256SUMS fi # download signed binary sha256 hash sum file and check if [ ! -f SHA256SUMS.asc ]; then - sudo -u joinmarket wget --progress=bar:force https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/SHA256SUMS.asc + sudo -u joinmarket wget --prefer-family=ipv4 --progress=bar:force https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/SHA256SUMS.asc fi verifyResult=$(gpg --verify SHA256SUMS.asc 2>&1) goodSignature=$(echo ${verifyResult} | grep 'Good signature' -c) @@ -44,7 +44,7 @@ function downloadBitcoinCore() { echo "correctKey(${correctKey})" if [ ${correctKey} -lt 1 ] || [ ${goodSignature} -lt 1 ]; then echo - echo "# !!! BUILD FAILED --> the PGP verification failed / signature(${goodSignature}) verify(${correctKey})" + echo "# BUILD FAILED --> the PGP verification failed / signature(${goodSignature}) verify(${correctKey})" echo "# Removing the conflicting files" echo "# This is normal after an update - try to choose the option / run the script again." sudo rm -f SHA256SUMS @@ -75,10 +75,10 @@ function downloadBitcoinCore() { # download resources binaryName="bitcoin-${bitcoinVersion}-${bitcoinOSversion}.tar.gz" if [ ! -f "./${binaryName}" ]; then - sudo -u joinmarket wget --progress=bar:force https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/${binaryName} + sudo -u joinmarket wget --prefer-family=ipv4 --progress=bar:force https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/${binaryName} fi if [ ! -f "./${binaryName}" ]; then - echo "!!! FAIL !!! Could not download the BITCOIN BINARY" + echo "# FAIL - Could not download the BITCOIN BINARY" exit 1 else # check binary checksum test @@ -89,7 +89,7 @@ function downloadBitcoinCore() { echo "Valid SHA256 checksum should be: ${bitcoinSHA256}" echo "Downloaded binary SHA256 checksum: ${binaryChecksum}" if [ "${binaryChecksum}" != "${bitcoinSHA256}" ]; then - echo "!!! FAIL !!! Downloaded BITCOIN BINARY not matching SHA256 checksum: ${bitcoinSHA256}" + echo "# FAIL - Downloaded BITCOIN BINARY does not match SHA256 checksum: ${bitcoinSHA256}" rm -v ./${binaryName} exit 1 else @@ -117,22 +117,22 @@ function installBitcoinCore() { cd /home/joinmarket/download/bitcoin-${bitcoinVersion}/bin/ || exit 1 sudo install -m 0755 -o root -g root -t /home/joinmarket/bitcoin ./* - if [ "$(grep -c "/home/joinmarket/bitcoin" < /home/joinmarket/.profile)" -eq 0 ];then + if [ "$(grep -c "/home/joinmarket/bitcoin" Was not able to install Bitcoin Core" + echo "# BUILD FAILED --> Was not able to install Bitcoin Core" exit 1 fi # bitcoin.conf if [ ! -f /home/joinmarket/.bitcoin/bitcoin.conf ]; then mkdir -p /home/joinmarket/.bitcoin - randomRPCpass=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c8) - cat > /home/joinmarket/.bitcoin/bitcoin.conf </home/joinmarket/.bitcoin/bitcoin.conf < function checkRPCwallet { getRPC - if [ $# -eq 0 ];then + if [ $# -eq 0 ]; then rpc_wallet=$rpc_wallet else rpc_wallet=$1 @@ -341,8 +341,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":false}}' \ + http://$rpc_user:$rpc_pass@$rpc_host:$rpc_port/wallet/$rpc_wallet | jq . echo walletFound=$(customRPC "# Check wallet" "listwallets" 2>$connectionOutput | grep -c "$rpc_wallet") if [ $walletFound -eq 0 ]; then @@ -360,23 +360,23 @@ function checkRPCwallet { # $1=id $2=method $3=string params $4=print function customRPC { getRPC - if [ ${#4} -eq 0 ];then + if [ ${#4} -eq 0 ]; then print="no" else print=$4 fi -if [ $print = print ];then - echo print -fi + if [ $print = print ]; then + echo print + fi tor="" if [ $(echo $rpc_host | grep -c .onion) -gt 0 ]; then tor="torsocks" echo "# Connecting over Tor..." echo fi - is_int () { test "$@" -eq "$@" 2> /dev/null; } - if is_int "$3" ||[ ${#3} -eq 0 ]; then - if [ $print = print ];then + is_int() { test "$@" -eq "$@" 2>/dev/null; } + if is_int "$3" || [ ${#3} -eq 0 ]; then + if [ $print = print ]; then echo "# Using the RPC command:" echo "$tor curl -sS --data-binary\ '{\"jsonrpc\": \"1.0\", \"id\":\"$1\", \"method\": \"$2\", \"params\": [$3] }'\ @@ -384,10 +384,10 @@ fi echo fi $tor curl -sS --data-binary \ - '{"jsonrpc": "1.0", "id":"'"$1"'", "method": "'"$2"'", "params": ['"$3"'] }' \ - http://$rpc_user:$rpc_pass@$rpc_host:$rpc_port/wallet/$rpc_wallet | jq . + '{"jsonrpc": "1.0", "id":"'"$1"'", "method": "'"$2"'", "params": ['"$3"'] }' \ + http://$rpc_user:$rpc_pass@$rpc_host:$rpc_port/wallet/$rpc_wallet | jq . else - if [ $print = print ];then + if [ $print = print ]; then echo "# Using the RPC command:" echo "$tor curl -sS --data-binary\ '{\"jsonrpc\": \"1.0\", \"id\":\"$1\", \"method\": \"$2\", \"params\": [\"$3\"] }'\ @@ -395,8 +395,8 @@ fi echo fi $tor curl -sS --data-binary \ - '{"jsonrpc": "1.0", "id":"'"$1"'", "method": "'"$2"'", "params": ["'"$3"'"] }' \ - http://$rpc_user:$rpc_pass@$rpc_host:$rpc_port/wallet/$rpc_wallet | jq . + '{"jsonrpc": "1.0", "id":"'"$1"'", "method": "'"$2"'", "params": ["'"$3"'"] }' \ + http://$rpc_user:$rpc_pass@$rpc_host:$rpc_port/wallet/$rpc_wallet | jq . fi } @@ -408,28 +408,28 @@ function connectLocalNode() { fi echo "# Setting connection to the local Bitcoin node on ${network}" rpc_host="127.0.0.1" - if [ "${network}" = mainnet ];then + if [ "${network}" = mainnet ]; then rpc_port="8332" - elif [ "${network}" = signet ];then + elif [ "${network}" = signet ]; then rpc_port="38332" - elif [ "${network}" = testnet ];then + elif [ "${network}" = testnet ]; then rpc_port="18332" fi rpc_wallet="wallet.dat" - if [ $runningEnv = raspiblitz ];then + if [ $runningEnv = raspiblitz ]; then rpc_user=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcuser | cut -c 9-) - rpc_pass=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf|grep rpcpassword|cut -c 13-) - elif [ $runningEnv = mynode ];then + rpc_pass=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcpassword | cut -c 13-) + elif [ $runningEnv = mynode ]; then rpc_user=mynode rpc_pass=$(sudo cat /mnt/hdd/mynode/settings/.btcrpcpw) - elif [ $runningEnv = standalone ];then - rpc_user=$(sudo cat /home/bitcoin/.bitcoin/bitcoin.conf|grep rpcuser|cut -c 9-) - rpc_pass=$(sudo cat /home/bitcoin/.bitcoin/bitcoin.conf|grep rpcpassword|cut -c 13-) + elif [ $runningEnv = standalone ]; then + rpc_user=$(sudo cat /home/bitcoin/.bitcoin/bitcoin.conf | grep rpcuser | cut -c 9-) + rpc_pass=$(sudo cat /home/bitcoin/.bitcoin/bitcoin.conf | grep rpcpassword | cut -c 13-) fi # set.bitcoinrpc.py python /home/joinmarket/set.bitcoinrpc.py --network=${network} \ - --rpc_user="$rpc_user" --rpc_pass="$rpc_pass" --rpc_host=$rpc_host \ - --rpc_port=$rpc_port --rpc_wallet=$rpc_wallet + --rpc_user="$rpc_user" --rpc_pass="$rpc_pass" --rpc_host=$rpc_host \ + --rpc_port=$rpc_port --rpc_wallet=$rpc_wallet # set joinin.conf value /home/joinmarket/set.value.sh set network ${network} ${joininConfPath} diff --git a/scripts/install.joinmarket.sh b/scripts/install.joinmarket.sh index 747cc69..eb33ee0 100755 --- a/scripts/install.joinmarket.sh +++ b/scripts/install.joinmarket.sh @@ -187,12 +187,12 @@ function installJoinMarket() { else sudo -u ${user} git reset --hard $testedJMversion - sudo -u ${user} wget -O "pgp_keys.asc" ${PGPpkeys} + 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 "# 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 key @@ -207,7 +207,7 @@ function installJoinMarket() { echo "# correctKey(${correctKey})" if [ ${correctKey} -lt 1 ] || [ ${goodSignature} -lt 1 ]; then echo - echo "# !!! BUILD FAILED --> PGP verification not OK / signature(${goodSignature}) verify(${correctKey})" + echo "# BUILD FAILED --> PGP verification not OK / signature(${goodSignature}) verify(${correctKey})" exit 1 else echo diff --git a/scripts/jam-remote/README.md b/scripts/jam-remote/README.md index 43767ef..ed49029 100644 --- a/scripts/jam-remote/README.md +++ b/scripts/jam-remote/README.md @@ -4,7 +4,7 @@ ### Download the repo ``` -git clone github.com/openoms/joininbox +git clone https://github.com/openoms/joininbox cd joininbox ``` diff --git a/scripts/jam-remote/bonus.nodejs.sh b/scripts/jam-remote/bonus.nodejs.sh index 33b02c3..b0d07fd 100755 --- a/scripts/jam-remote/bonus.nodejs.sh +++ b/scripts/jam-remote/bonus.nodejs.sh @@ -55,9 +55,9 @@ if [ "$1" = "1" ] || [ "$1" = "on" ]; then # download mkdir download cd download || exit 1 - wget -O node-$VERSION-$DISTRO.tar.xz https://nodejs.org/dist/$VERSION/node-$VERSION-$DISTRO.tar.xz + wget --prefer-family=ipv4 -O node-$VERSION-$DISTRO.tar.xz https://nodejs.org/dist/$VERSION/node-$VERSION-$DISTRO.tar.xz # checksum - wget -O SHASUMS256.txt https://nodejs.org/dist/$VERSION/SHASUMS256.txt + wget --prefer-family=ipv4 -O SHASUMS256.txt https://nodejs.org/dist/$VERSION/SHASUMS256.txt if ! sha256sum -c SHASUMS256.txt --ignore-missing; then echo "FAIL: The checksum of node-$VERSION-$DISTRO.tar.xz is not found in the SHASUMS256.txt" rm -f node-$VERSION-$DISTRO.tar.xz* diff --git a/scripts/menu.config.sh b/scripts/menu.config.sh index 3242ccc..093daea 100755 --- a/scripts/menu.config.sh +++ b/scripts/menu.config.sh @@ -25,7 +25,7 @@ if [ "$runningEnv" = "standalone" ] && [ "$setupStep" -lt 100 ]; then # BASIC MENU INFO HEIGHT=16 - WIDTH=64 + WIDTH=68 CHOICE_HEIGHT=24 TITLE="Startup options" MENU=" @@ -38,7 +38,7 @@ if [ "$runningEnv" = "standalone" ] && [ "$setupStep" -lt 100 ]; then OPTIONS+=( CONNECT "Connect to a remote bitcoin node on mainnet" SIGNET "Start on signet with a local Bitcoin Core" - PRUNED "Start a pruned node from prunednode.today") + PRUNED "Start a pruned node from pruned.host4coins.net/blocks") if [ -f /home/bitcoin/.bitcoin/bitcoin.conf ];then OPTIONS+=( LOCAL "Connect to the local Bitcoin Core on mainnet") @@ -54,7 +54,7 @@ if [ "$runningEnv" = "standalone" ] && [ "$setupStep" -lt 100 ]; then else # BASIC MENU INFO HEIGHT=12 - WIDTH=64 + WIDTH=68 CHOICE_HEIGHT=20 TITLE="Configuration options" MENU="" @@ -70,7 +70,7 @@ else SIGNET "Switch to signet with a local Bitcoin Core") if [ "${runningEnv}" = standalone ]; then OPTIONS+=( - PRUNED "Start a pruned node locally from prunednode.today") + PRUNED "Start a pruned node from pruned.host4coins.net/blocks") HEIGHT=$((HEIGHT+1)) CHOICE_HEIGHT=$((CHOICE_HEIGHT+1)) fi diff --git a/scripts/standalone/_functions.standalone.sh b/scripts/standalone/_functions.standalone.sh index f537ca2..d889505 100755 --- a/scripts/standalone/_functions.standalone.sh +++ b/scripts/standalone/_functions.standalone.sh @@ -1,7 +1,7 @@ #!/bin/bash function addUserStore() { - if [ ! -d /home/store/app-data ];then + if [ ! -d /home/store/app-data ]; then echo "# Adding the user: store" sudo adduser --disabled-password --gecos "" store sudo -u store mkdir /home/store/app-data @@ -11,7 +11,7 @@ function addUserStore() { } function moveSignetData() { - if [ -f /etc/systemd/system/signetd.service ];then + if [ -f /etc/systemd/system/signetd.service ]; then sudo systemctl stop signetd sudo mv /home/joinmarket/.bitcoin /home/store/app-data/ sudo ln -s /home/store/app-data/.bitcoin /home/bitcoin/ @@ -20,7 +20,7 @@ function moveSignetData() { fi } -function makeEncryptedFolder(){ +function makeEncryptedFolder() { # TODO # make encrypted file @@ -33,9 +33,21 @@ function makeEncryptedFolder(){ } function downloadSnapShot() { + + if [ $# -eq 0 ] || [ "$1" = "pruned.host4coins.net" ]; then + hashFileName="sha256sum.txt" + hashFileSigName="sha256sum.txt.asc" + downloadDomain="pruned.host4coins.net/blocks" + pgpKeyLink="https://keys.openpgp.org/vks/v1/by-fingerprint/440C15769D19E6908CC1DDB23070DE9772DB8A48" + elif [ "$1" = "prunednode.today" ]; then + hashFileName="latest.signed.txt" + downloadDomain="prunednode.today" + pgpKeyLink="https://stepansnigirev.com/ss-specter-release.asc" + fi + echo "# Check available diskspace" FREE=$(df -k --output=avail "$PWD" | tail -n1) # df -k not df -h - if [ $FREE -lt 12582912 ];then # 12G = 12*1024*1024k + if [ $FREE -lt 12582912 ]; then # 12G = 12*1024*1024k echo "# The free space is only $FREE bytes!" echo "# Would need ~12GB free space to download and extract the snapshot." echo "# Press ENTER to continue to download regardless or CTRL+C to exit." @@ -45,20 +57,22 @@ function downloadSnapShot() { fi sudo -u joinmarket mkdir /home/joinmarket/download 2>/dev/null cd /home/joinmarket/download || exit 1 - hashFileName="latest.signed.txt" - sudo rm $hashFileName - wget https://prunednode.today/$hashFileName || exit 1 - downloadFileName=$(grep .zip < $hashFileName | awk '{print $2}') - downloadLink="https://prunednode.today/$downloadFileName" - echo "# Import the PGP keys of Stepan Snigirev" - curl https://stepansnigirev.com/ss-specter-release.asc | gpg --import || exit 1 + sudo rm $hashFileName 2>/dev/null + echo "# Downloading $hashFileName ..." + wget --prefer-family=ipv4 -O $hashFileName https://$downloadDomain/$hashFileName || exit 1 + + downloadFileName=$(grep .zip <$hashFileName | awk '{print $2}') + downloadLink="https://$downloadDomain/$downloadFileName" + + echo "# Import the signing key" + curl -sS "$pgpKeyLink" | gpg --import || exit 1 echo "# Verifying the signature of the hash ..." - verifyResult=$(gpg --verify $hashFileName 2>&1) - goodSignature=$(echo ${verifyResult} | grep -c 'Good signature') - echo "# goodSignature (${goodSignature})" - if [ ${goodSignature} -eq 0 ];then + if [ ${#hashFileSigName} -gt 0 ]; then + wget --prefer-family=ipv4 -O $hashFileSigName https://$downloadDomain/$hashFileSigName || exit 1 + fi + if ! gpg --verify $hashFileSigName $hashFileName; then echo "# Invalid signature on $hashFileName" echo "# Press ENTER to remove the invalid file or CTRL+C to abort." read key @@ -67,18 +81,18 @@ function downloadSnapShot() { exit 1 fi - if [ ! -f $downloadFileName ];then + if [ ! -f $downloadFileName ]; then echo echo "# Downloading $downloadLink ..." echo - wget $downloadLink + wget --prefer-family=ipv4 -O $downloadFileName $downloadLink || exit 1 fi echo "# Verifying the hash (takes time) ..." - verifyHash=$(sha256sum -c $hashFileName 2>&1) - goodHash=$(echo ${verifyHash} | grep -c OK) - echo "# goodHash ($goodHash)" - if [ ${goodSignature} -lt 1 ] || [ ${goodHash} -lt 1 ]; then + if sha256sum -c $hashFileName --ignore-missing; then + echo + echo "# The PGP signature and the hash of the downloaded snapshot is correct" + else echo echo "# Download failed --> the PGP signature did not match / signature(${goodSignature})" echo "# Press ENTER to remove the invalid files or CTRL+C to abort." @@ -87,14 +101,11 @@ function downloadSnapShot() { rm -f $hashFileName rm -f $downloadFileName exit 1 - else - echo - echo "# The PGP signature and the hash of the downloaded snapshot is correct" fi - echo "# Exracting to /home/store/app-data/.bitcoin ..." + echo "# Extracting to /home/store/app-data/.bitcoin ..." FREE=$(df -k --output=avail "$PWD" | tail -n1) # df -k not df -h - if [ $FREE -lt 7340032 ];then # 7G = 7*1024*1024k + if [ $FREE -lt 7340032 ]; then # 7G = 7*1024*1024k echo "# The free space is only $FREE bytes!" echo "# Would need ~7GB free space to extract the snapshot." echo "# Press ENTER to continue to download regardless or CTRL+C to exit." @@ -103,31 +114,38 @@ function downloadSnapShot() { echo "# OK, more than 7GB is free!" fi addUserStore - sudo mkdir -p /home/store/app-data/.bitcoin - echo "# Make sure bitcoind is not running" - sudo systemctl stop bitcoind - if [ -f /home/bitcoin/.bitcoin/bitcoin.conf ];then - echo "# Back up bitcoin.conf" - sudo -u bitcoin mv /home/bitcoin/.bitcoin/bitcoin.conf \ - /home/bitcoin/.bitcoin/bitcoin.conf.backup - fi - echo "# Check unzip" - sudo apt-get install -y unzip - sudo unzip -o $downloadFileName -d /home/store/app-data/.bitcoin - if [ -f /home/bitcoin/.bitcoin/bitcoin.conf.backup ];then - echo "# Restore bitcoin.conf" - sudo -u bitcoin mv -f /home/bitcoin/.bitcoin/bitcoin.conf.backup \ - /home/bitcoin/.bitcoin/bitcoin.conf + if [ ! -d /home/store/app-data/.bitcoin ]; then + sudo mkdir -p /home/store/app-data/.bitcoin fi echo "# Making sure user: bitcoin exists" sudo adduser --disabled-password --gecos "" bitcoin sudo chown -R bitcoin:bitcoin /home/store/app-data/.bitcoin + echo "# Make sure bitcoind is not running" + sudo systemctl stop bitcoind + if [ -f /home/bitcoin/.bitcoin/bitcoin.conf ]; then + echo "# Back up bitcoin.conf" + sudo -u bitcoin mv /home/bitcoin/.bitcoin/bitcoin.conf \ + /home/bitcoin/.bitcoin/bitcoin.conf.backup + fi + + echo "# Clean old blocks and chainstate" + sudo rm -rf /home/store/app-data/.bitcoin/blocks + sudo rm -rf /home/store/app-data/.bitcoin/chainstate + + echo "# Unzip ..." + sudo apt-get install -y unzip + sudo -u bitcoin unzip -o $downloadFileName -d /home/store/app-data/.bitcoin + if [ -f /home/bitcoin/.bitcoin/bitcoin.conf.backup ]; then + echo "# Restore bitcoin.conf" + sudo -u bitcoin mv -f /home/bitcoin/.bitcoin/bitcoin.conf.backup \ + /home/bitcoin/.bitcoin/bitcoin.conf + fi } function installBitcoinCoreStandalone() { downloadBitcoinCore - if [ -f /home/bitcoin/bitcoin/bitcoind ];then + if [ -f /home/bitcoin/bitcoin/bitcoind ]; then installedVersion=$(/home/bitcoin/bitcoin/bitcoind --version | grep version) echo "${installedVersion} is already installed" else @@ -138,20 +156,19 @@ function installBitcoinCoreStandalone() { cd /home/joinmarket/download/bitcoin-${bitcoinVersion}/bin/ || exit 1 sudo install -m 0755 -o root -g root -t /home/bitcoin/bitcoin ./* fi - if [ "$(grep -c "/home/bitcoin/bitcoin" < /etc/profile)" -eq 0 ];then + if [ "$(grep -c "/home/bitcoin/bitcoin" Was not able to install Bitcoin Core)" + echo "# BUILD FAILED --> Was not able to install Bitcoin Core)" exit 1 fi # bitcoin.conf - if [ -f /home/store/app-data/.bitcoin/bitcoin.conf ];then - if [ $(grep -c rpcpassword < /home/store/app-data/.bitcoin/bitcoin.conf) -eq 0 ];then + if [ -f /home/store/app-data/.bitcoin/bitcoin.conf ]; then + if [ $(grep -c rpcpassword > /home/joinmarket/_aliases.sh" sudo bash -c "echo 'alias bitcoind=\"sudo -u bitcoin /home/bitcoin/bitcoin/bitcoind\"' >> /home/joinmarket/_aliases.sh" fi @@ -234,7 +251,7 @@ 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 + if [ ! -f /home/bitcoin/.bitcoin/mainnet/wallets/wallet.dat/wallet.dat ]; then echo "# Create wallet.dat ..." sleep 10 sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli -named createwallet wallet_name=wallet.dat descriptors=false diff --git a/scripts/verify.git.sh b/scripts/verify.git.sh index 6bfee05..a687c04 100644 --- a/scripts/verify.git.sh +++ b/scripts/verify.git.sh @@ -30,7 +30,7 @@ PGPsigner="$1" PGPpubkeyLink="$2" PGPpubkeyFingerprint="$3" -wget -O /dev/shm/pgp_keys_${PGPsigner}.asc "${PGPpubkeyLink}" +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) if [ "${fingerprint}" -lt 1 ]; then From b1e933d013b5c1450f3586b714ad8a7786f2b97c Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:06:12 +0000 Subject: [PATCH 08/56] ci: update arm64-rpi base image (#132) --- ci/arm64-rpi/arm64-rpi.pkr.hcl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/arm64-rpi/arm64-rpi.pkr.hcl b/ci/arm64-rpi/arm64-rpi.pkr.hcl index 97c6e19..4f7c294 100644 --- a/ci/arm64-rpi/arm64-rpi.pkr.hcl +++ b/ci/arm64-rpi/arm64-rpi.pkr.hcl @@ -3,10 +3,10 @@ variable "branch" {} source "arm" "joininbox-arm64-rpi" { file_checksum_type = "sha256" - file_checksum = "b77cf8a9d46ce1be16a7a478c243b2197be5c217482db2bfee10fc94764c3805" + file_checksum = "a68cd2bfe7831c438d8a5d832803ae0db17afec9f3cd370d9e8748c7b5456283" file_target_extension = "xz" file_unarchive_cmd = ["xz", "--decompress", "$ARCHIVE_PATH"] - file_urls = ["https://raspi.debian.net/tested/20230101_raspi_4_bookworm.img.xz"] + file_urls = ["https://raspi.debian.net/tested/20230612_raspi_4_bookworm.img.xz"] image_build_method = "resize" image_chroot_env = ["PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin"] image_partitions { From ec556ab64ac0a223c8a4476fca66662575e08449 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:08:58 +0000 Subject: [PATCH 09/56] update bitcoin core to v25.0 (#133) * update bitcoin core to v25.0 * fix version check * add the joinmarket user to the bitcon group --- scripts/_functions.bitcoincore.sh | 53 +++++++-------------- scripts/standalone/_functions.standalone.sh | 15 ++++-- 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/scripts/_functions.bitcoincore.sh b/scripts/_functions.bitcoincore.sh index f9f415c..5ab5b5c 100755 --- a/scripts/_functions.bitcoincore.sh +++ b/scripts/_functions.bitcoincore.sh @@ -9,11 +9,12 @@ joininConfPath="/home/joinmarket/joinin.conf" function downloadBitcoinCore() { # set version # https://bitcoincore.org/en/download/ - bitcoinVersion="22.0" + bitcoinVersion="25.0" - # https://github.com/laanwj - PGPname="Wladimir J. van der Laan" - PGPpubkey="74810B012346C9A6" + if bitcoin-cli --version | grep $bitcoinVersion >/dev/null; then + echo "# Bitcoin Core $bitcoinVersion is already installed" + return 0 + fi echo echo "# *** PREPARING BITCOIN ***" @@ -21,41 +22,25 @@ function downloadBitcoinCore() { sudo -u joinmarket mkdir /home/joinmarket/download 2>/dev/null cd /home/joinmarket/download || exit 1 - # receive signer key - if ! gpg -k | grep "${PGPpubkey}"; then - if ! gpg --keyserver hkp://keyserver.ubuntu.com --recv-key "${PGPpubkey}"; then - echo "# FAIL - Couldn't download ${PGPname}'s PGP pubkey" - exit 1 - fi - fi + echo "# Receive signer keys" + curl -s "https://api.github.com/repos/bitcoin-core/guix.sigs/contents/builder-keys" | + jq -r '.[].download_url' | while read url; do curl -s "$url" | gpg --import; done # download signed binary sha256 hash sum file - if [ ! -f SHA256SUMS ]; then - sudo -u joinmarket wget --prefer-family=ipv4 --progress=bar:force https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/SHA256SUMS - fi - # download signed binary sha256 hash sum file and check - if [ ! -f SHA256SUMS.asc ]; then - sudo -u joinmarket wget --prefer-family=ipv4 --progress=bar:force https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/SHA256SUMS.asc - fi - verifyResult=$(gpg --verify SHA256SUMS.asc 2>&1) - goodSignature=$(echo ${verifyResult} | grep 'Good signature' -c) - echo "goodSignature(${goodSignature})" - correctKey=$(echo ${verifyResult} | tr -d " \t\n\r" | grep "${PGPpubkey}" -c) - echo "correctKey(${correctKey})" - if [ ${correctKey} -lt 1 ] || [ ${goodSignature} -lt 1 ]; then - echo - echo "# BUILD FAILED --> the PGP verification failed / signature(${goodSignature}) verify(${correctKey})" - echo "# Removing the conflicting files" - echo "# This is normal after an update - try to choose the option / run the script again." - sudo rm -f SHA256SUMS - sudo rm -f SHA256SUMS.asc - exit 1 - else + sudo -u joinmarket wget --prefer-family=ipv4 --progress=bar:force -O SHA256SUMS https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/SHA256SUMS + # download the signed binary sha256 hash sum file and check + sudo -u joinmarket wget --prefer-family=ipv4 --progress=bar:force -O SHA256SUMS.asc https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/SHA256SUMS.asc + + if gpg --verify SHA256SUMS.asc; then echo echo "****************************************" echo "OK --> BITCOIN MANIFEST IS CORRECT" echo "****************************************" echo + else + echo + echo "# BUILD FAILED --> the PGP verification failed / signature(${goodSignature}) " + exit 1 fi # detect CPU architecture & fitting download link @@ -98,8 +83,6 @@ function downloadBitcoinCore() { echo "OK --> VERIFIED BITCOIN CORE BINARY CHECKSUM" echo "********************************************" echo - sleep 10 - echo fi echo echo "# Extracting to /home/joinmarket/download/bitcoin-${bitcoinVersion}" @@ -332,7 +315,7 @@ function checkRPCwallet { 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 for the remote Bitcoin Core named $rpc_wallet" + echo "# Setting a watch only wallet in Bitcoin Core named $rpc_wallet" tor="" if [ $(echo $rpc_host | grep -c .onion) -gt 0 ]; then tor="torsocks" diff --git a/scripts/standalone/_functions.standalone.sh b/scripts/standalone/_functions.standalone.sh index d889505..42939e5 100755 --- a/scripts/standalone/_functions.standalone.sh +++ b/scripts/standalone/_functions.standalone.sh @@ -5,6 +5,8 @@ function addUserStore() { echo "# Adding the user: store" sudo adduser --disabled-password --gecos "" store sudo -u store mkdir /home/store/app-data + echo "# Add the joinmarket user to the store group" + sudo usermod -aG store joinmarket else echo "# The folder /home/store/app-data is present already" fi @@ -120,9 +122,11 @@ function downloadSnapShot() { echo "# Making sure user: bitcoin exists" sudo adduser --disabled-password --gecos "" bitcoin sudo chown -R bitcoin:bitcoin /home/store/app-data/.bitcoin + echo "# Add the joinmarket user to the bitcoin group" + sudo usermod -aG bitcoin joinmarket echo "# Make sure bitcoind is not running" sudo systemctl stop bitcoind - if [ -f /home/bitcoin/.bitcoin/bitcoin.conf ]; then + if sudo -u bitcoin ls /home/bitcoin/.bitcoin/bitcoin.conf; then echo "# Back up bitcoin.conf" sudo -u bitcoin mv /home/bitcoin/.bitcoin/bitcoin.conf \ /home/bitcoin/.bitcoin/bitcoin.conf.backup @@ -135,7 +139,7 @@ function downloadSnapShot() { echo "# Unzip ..." sudo apt-get install -y unzip sudo -u bitcoin unzip -o $downloadFileName -d /home/store/app-data/.bitcoin - if [ -f /home/bitcoin/.bitcoin/bitcoin.conf.backup ]; then + if sudo -u bitcoin ls /home/bitcoin/.bitcoin/bitcoin.conf.backup; then echo "# Restore bitcoin.conf" sudo -u bitcoin mv -f /home/bitcoin/.bitcoin/bitcoin.conf.backup \ /home/bitcoin/.bitcoin/bitcoin.conf @@ -151,6 +155,8 @@ function installBitcoinCoreStandalone() { else echo "# Adding the user: bitcoin" sudo adduser --disabled-password --gecos "" bitcoin + echo "# Add the joinmarket user to the bitcoin group" + sudo usermod -aG bitcoin joinmarket echo "# Installing Bitcoin Core v${bitcoinVersion}" sudo -u bitcoin mkdir -p /home/bitcoin/bitcoin cd /home/joinmarket/download/bitcoin-${bitcoinVersion}/bin/ || exit 1 @@ -168,7 +174,8 @@ function installBitcoinCoreStandalone() { # bitcoin.conf if [ -f /home/store/app-data/.bitcoin/bitcoin.conf ]; then - if [ $(grep -c rpcpassword Date: Fri, 16 Jun 2023 14:34:57 +0000 Subject: [PATCH 10/56] docs: update readme and screenshots (#134) --- README.md | 21 +++++++++++++++------ images/menu.png | Bin 73726 -> 62242 bytes images/menu.startup.png | Bin 38724 -> 38037 bytes images/menu.tools.png | Bin 41440 -> 43859 bytes 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c7708db..3588164 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ 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) + - [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) - [Tested environments](#tested-environments) @@ -40,7 +43,7 @@ A minimalistic, security focused linux environment for JoinMarket with a termina * Connect remotely to a Bitcoin Core node * 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://prunednode.today/ +* Start a pruned node from https://pruned.host4coins.net/blocks * JoininBox is part the RaspiBlitz SERVICES **The addresses, transactions and balances of JoinMarket can be seen in the watch-only wallet of the connected node.** @@ -48,12 +51,18 @@ A minimalistic, security focused linux environment for JoinMarket with a termina * to protect privacy in case of physical access use disk encryption ## Required Hardware -* RaspberryPi 4 ([alternatively any other computer running a Debian Linux flavour](#tested-environments)) +### A computer running a Debian / Ubuntu Linux flavour. +* See the [tested-environments](#tested-environments). +### RaspberryPi 4 or 3 * Power supply (5V 3A and above recommended) * Heatsink case -* 16GB SDcard (minimum) - 32GB to use a pruned node +* 32 GB Endurence type SDcard * [(USB SSD to run a pruned bitcoin node locally)](FAQ.md#usb-ssd-recommendation) -* [other tested hardware ](#tested-environments) +### VPS eg: [host4coins.net](https://host4coins.net/) +Recommended minimum: +* 1 GB RAM +* 1 vCPU +* 32 GB SSD **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 used to recover the funds!** @@ -87,7 +96,7 @@ A minimalistic, security focused linux environment for JoinMarket with a termina * next will be presented with the CONFIG menu to * Connect to a remote bitcoin node on mainnet * Try JoinMarket on signet - * Start a pruned node from [prunednode.today](https://prunednode.today) + * Start a pruned node from [pruned.host4coins.net/blocks](https://pruned.host4coins.net/blocks) * Edit the joinmarket.cfg manually * Update JoininBox or JoinMarket @@ -105,7 +114,7 @@ A minimalistic, security focused linux environment for JoinMarket with a termina * Latest * Debian Bullseye X86_64 desktop ([images and logs available in GitHub actions](https://github.com/openoms/joininbox/actions?query=workflow%3Aamd64-image-build++branch%3Amaster+is%3Asuccess)) * Raspberry Pi 4 running 64bit Debian Bullseye ([images and logs available in GitHub actions](https://github.com/openoms/joininbox/actions?query=workflow%3Aarm64-rpi-image-build++branch%3Amaster+is%3Asuccess++)) - * Ubuntu 22.04 X86_64 desktop (virtual machine) + * Ubuntu 22.04 Jammy X86_64 desktop (virtual machine) * Previous versions tested * [Raspberry Pi 4 running 64bit Debian Buster](FAQ.md#build-the-sdcard-image) * [Hardkernel Odroid XU4/HC1 running 32bit Armbian Buster](FAQ.md#set-up-armbian-on-the-hardkernel-odroid-xu4) diff --git a/images/menu.png b/images/menu.png index a51f13c6fd26315384e96b22eec5fe9747f5259c..cf13cd0408951d89b7dc7a95cd22f12bbcfca731 100644 GIT binary patch literal 62242 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
y!83nt1gTY) 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 Ak^lez diff --git a/images/menu.startup.png b/images/menu.startup.png index fa66900dcc0356eb04aa0a4a33cf236124e74f65..8459ac602ad878ba84576b102d4e5b942745f1cd 100644 GIT binary patch literal 38037 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 diff --git a/images/menu.tools.png b/images/menu.tools.png index 988f0f0fe2864e62ddca0c2a043faf87b38e61a1..1ac74da4ee83f8034c674d015cdb4da2f18ac862 100644 GIT binary patch literal 43859 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 literal 41440 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 Date: Sun, 23 Jul 2023 06:34:20 +0000 Subject: [PATCH 11/56] update amd64 base image to debian 12.1.0 (#137) --- ci/amd64/debian/joininbox-amd64-debian.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index ae73dd8..539f784 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -1,5 +1,7 @@ { "variables": { + "iso_name": "debian-12.1.0-amd64-netinst.iso", + "iso_checksum": "9f181ae12b25840a508786b1756c6352a0e58484998669288c4eec2ab16b8559", "box_basename": "debian", "build_directory": "../builds", "build_timestamp": "{{isotime \"20060102150405\"}}", @@ -11,8 +13,6 @@ "http_directory": "{{template_dir}}/http", "http_proxy": "{{env `http_proxy`}}", "https_proxy": "{{env `https_proxy`}}", - "iso_checksum": "3b0e9718e3653435f20d8c2124de6d363a51a1fd7f911b9ca0c6db6b3d30d53e", - "iso_name": "debian-12.0.0-amd64-netinst.iso", "memory": "2048", "mirror": "http://cdimage.debian.org/cdimage/release", "mirror_directory": "current/amd64/iso-cd", From 709ea134dc977e3ceece3ce709e7e88123ff8bd1 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 23 Jul 2023 06:35:04 +0000 Subject: [PATCH 12/56] fix typos and add spelling (#136) * chore: fix typos * ci: add spelling gh action * chore: fix false positives --- .github/workflows/spelling.yml | 17 +++++++++++++++++ FAQ.md | 4 ++-- build_joininbox.sh | 4 ++-- prepare_remote_node.md | 8 ++++---- release_notes.md | 4 ++-- scripts/_functions.menu.sh | 4 ++-- scripts/_functions.sh | 2 +- scripts/menu.payjoin.sh | 4 ++-- scripts/menu.send.sh | 4 ++-- typos.toml | 10 ++++++++++ 10 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/spelling.yml create mode 100644 typos.toml diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml new file mode 100644 index 0000000..82a8003 --- /dev/null +++ b/.github/workflows/spelling.yml @@ -0,0 +1,17 @@ +name: spelling + +on: + pull_request: + branches: [ master ] + +jobs: + spelling: + name: Spell Check with Typos + runs-on: ubuntu-latest + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v3 + - name: Spell Check Repo + uses: crate-ci/typos@master + with: + config: typos.toml diff --git a/FAQ.md b/FAQ.md index 053ada2..97ea6c7 100644 --- a/FAQ.md +++ b/FAQ.md @@ -205,7 +205,7 @@ https://www.armbian.com/odroid-xu4/ * Continue to [install JoininBox](README.md#install-joininbox) ## Download and verify Raspbian SDcard image for a Raspberry Pi -To be able to open the JoinMarket-QT GUI on the dekstop from the RPI +To be able to open the JoinMarket-QT GUI on the desktop from the RPI need to use the Raspberry Pi OS (32-bit) with desktop image * Download image: https://downloads.raspberrypi.org/raspios_armhf/images/raspios_armhf-2020-05-28/2020-05-27-raspios-buster-armhf.zip @@ -490,7 +490,7 @@ You need to run rescanblockchain on that wallet in bitcoind after importing the * To rescan on the node run (https://developer.bitcoin.org/reference/rpc/rescanblockchain.html?highlight=rescanblockchain): `bitcoin-cli -rpcwallet=joininbox rescanblockchain 477120` -Rescanning fom the first SegWit block is sufficient for the default SegWit wallets. +Rescanning from the first SegWit block is sufficient for the default SegWit wallets. * Monitor progress (on a RaspiBlitz): `sudo tail -fn 100 /mnt/hdd/bitcoin/debug.log` diff --git a/build_joininbox.sh b/build_joininbox.sh index 7af37fa..9750546 100644 --- a/build_joininbox.sh +++ b/build_joininbox.sh @@ -5,7 +5,7 @@ # https://github.com/openoms/joininbox#tested-environments-for-joininbox # login with SSH or boot directly # run this script as root or with sudo -# can specify donwloading from a branch or forked repo: +# can specify downloading from a branch or forked repo: # bash build_joininbox.sh [branch] [github user] ######################################################################## @@ -497,7 +497,7 @@ deb-src [arch=${arch}] https://deb.torproject.org/torproject.org ${distro} main" debuild -rfakeroot -uc -us cd .. || exit 1 dpkg -i tor_*.deb - # setup Tor in the backgound + # setup Tor in the background # TODO - test if remains in the background after the Tor service is started tor & else diff --git a/prepare_remote_node.md b/prepare_remote_node.md index 130343e..8c2fe41 100644 --- a/prepare_remote_node.md +++ b/prepare_remote_node.md @@ -1,7 +1,7 @@ # Prepare a bitcoin node to accept a remote RPC connection -If you have arrived here from runing JoininBox locally on the RaspiBlitz can skip these steps (CTRL+C to exit the connection settings). The connection to the local bitcoin node is set up automatically. +If you have arrived here from running JoininBox locally on the RaspiBlitz can skip these steps (CTRL+C to exit the connection settings). The connection to the local bitcoin node is set up automatically. JoinMarket (running in JoininBox) needs to connect to Bitcoin Core via RPC. A pruned node with the **bitcoind wallet enabled** will do and txindex is not required. @@ -9,13 +9,13 @@ A pruned node with the **bitcoind wallet enabled** will do and txindex is not re - [Enable the bitcoind wallet](#enable-the-bitcoind-wallet) - [LAN connection](#lan-connection) - [Tor connection](#tor-connection) - - [CONFIG -> CONNECT in JoininBox](#config---connect-in-joininbox) + - [CONFIG -\> CONNECT in JoininBox](#config---connect-in-joininbox) - [RoninDojo](#ronindojo) - [Enable the bitcoind wallet](#enable-the-bitcoind-wallet-1) - [LAN connection](#lan-connection-1) - [Tor connection](#tor-connection-1) - [Recompile the Docker containers (takes time)](#recompile-the-docker-containers-takes-time) - - [CONFIG -> CONNECT in JoininBox](#config---connect-in-joininbox-1) + - [CONFIG -\> CONNECT in JoininBox](#config---connect-in-joininbox-1) - [Resources](#resources) ## RaspiBlitz @@ -48,7 +48,7 @@ This can be skipped if you [connect through Tor](#tor-connection) * 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 * `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` entires especially for the localhost: `127.0.0.1` + * can keep the other `rpcallowip` and `rpcbind` entries especially for the localhost: `127.0.0.1` Example: ```bash diff --git a/release_notes.md b/release_notes.md index e1a8f27..c1b7dad 100644 --- a/release_notes.md +++ b/release_notes.md @@ -128,7 +128,7 @@ RaspiBlitz integration: - pair the parallel 2nd node to Sphinx, BoS, ThunderHub, BTCPay - see: https://github.com/rootzoll/raspiblitz/issues/2073 Standalone: -- ElectRS install scritpt to be used with a local disk (requires a non-pruned node) - [FAQ](https://github.com/openoms/joininbox/blob/master/FAQ.md#external-drive) +- ElectRS install script to be used with a local disk (requires a non-pruned node) - [FAQ](https://github.com/openoms/joininbox/blob/master/FAQ.md#external-drive) ``` 369c883 payjoin: extend dialog window to content @@ -263,7 +263,7 @@ b047395 (menu-improvements) getRPC values in the customRPC function 0e08c25 add XPUBS option to the WALLET menu 840094f specter: update to 1.2.2 1f60cfc add bitcoind LOGS to TOOLS -bf4672d specter insall fixes +bf4672d specter install fixes 16236e9 add SPECTER menu with UPDATE option fa71818 add PRUNED node and LOCAL option c3336cd implement checkRPC to create and load core wallet diff --git a/scripts/_functions.menu.sh b/scripts/_functions.menu.sh index af0c6b8..a4b1519 100644 --- a/scripts/_functions.menu.sh +++ b/scripts/_functions.menu.sh @@ -66,7 +66,7 @@ It is best to reset the joinmarket.cfg after every install. Do you want to reset the joinmarket.cfg to the defaults now? " 12 65 - # make decison + # make decision pressed=$? case $pressed in 0) @@ -87,7 +87,7 @@ dialog --backtitle "Connect to the local Bitcoin Core" \ --title "Connect to the local Bitcoin Core" \ --yesno " Do you want to connect to the local Bitcoin Core on mainnet now?" 7 55 - # make decison + # make decision pressed=$? case $pressed in 0) diff --git a/scripts/_functions.sh b/scripts/_functions.sh index 289e536..c7541b6 100755 --- a/scripts/_functions.sh +++ b/scripts/_functions.sh @@ -66,7 +66,7 @@ function passwordToFile() { --title "Enter password" \ --insecure \ --passwordbox "Type or paste the wallet decryption password" 8 52 2> "$data" - # make decison + # make decision pressed=$? case $pressed in 0) diff --git a/scripts/menu.payjoin.sh b/scripts/menu.payjoin.sh index 6b9a67e..9ee9467 100755 --- a/scripts/menu.payjoin.sh +++ b/scripts/menu.payjoin.sh @@ -47,7 +47,7 @@ $(echo $(cat $wallet) | sed "s#$walletPath##g") mixdepth: $(cat $mixdepth) " 12 55 -# make decison +# make decision pressed=$? case $pressed in 0) @@ -141,7 +141,7 @@ mixdepth: $(cat "$mixdepth") Miner fee: $txfeeMessage " 16 55 -# make decison +# make decision pressed=$? case $pressed in 0) diff --git a/scripts/menu.send.sh b/scripts/menu.send.sh index 8ea0a8b..57ff255 100755 --- a/scripts/menu.send.sh +++ b/scripts/menu.send.sh @@ -133,7 +133,7 @@ Paste the destination address" 19 69 2> "$address" openMenuIfCancelled $? # changeAddress -trap 'rm -f "$changeAdress"' EXIT +trap 'rm -f "$changeAddress"' EXIT changeAddress=$(mktemp -p /dev/shm/) if [ "$amountsats" != "0 sats" ]; then dialog --backtitle "Custom change address" \ @@ -185,7 +185,7 @@ $(cat "$address") change address (optional): $changeAddressMessage" 17 69 -# make decison +# make decision pressed=$? case $pressed in 0) diff --git a/typos.toml b/typos.toml new file mode 100644 index 0000000..46ea55e --- /dev/null +++ b/typos.toml @@ -0,0 +1,10 @@ +# curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +# source "$HOME/.cargo/env" +# cargo install typos-cli +# typos -c typos.toml +# typos -c typos.toml -w + +[default.extend-words] +# don't correct these false positives +ba = "ba" +ned = "ned" \ No newline at end of file From accf27473009388008826cfd6de6ff673ae8e208 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Thu, 31 Aug 2023 08:39:11 +0200 Subject: [PATCH 13/56] joinmarket update to v0.9.10 (#138) --- scripts/install.joinmarket.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/install.joinmarket.sh b/scripts/install.joinmarket.sh index eb33ee0..5de3248 100755 --- a/scripts/install.joinmarket.sh +++ b/scripts/install.joinmarket.sh @@ -1,9 +1,14 @@ #!/bin/bash -testedJMversion="v0.9.9" -PGPsigner="waxwing" -PGPpkeys="https://raw.githubusercontent.com/JoinMarket-Org/joinmarket-clientserver/master/pubkeys/AdamGibson.asc" -PGPcheck="2B6FC204D9BF332D062B461A141001A1AF77F20B" +testedJMversion="v0.9.10" + +PGPsigner="kristapsk" +PGPpkeys="https://github.com/kristapsk.gpg" +PGPcheck="33E472FE870C7E5D" + +#PGPsigner="waxwing" +#PGPpkeys="https://raw.githubusercontent.com/JoinMarket-Org/joinmarket-clientserver/master/pubkeys/AdamGibson.asc" +#PGPcheck="2B6FC204D9BF332D062B461A141001A1AF77F20B" me="${0##/*}" From a596e25ce03adfe48cf465fc01c4757167452ac1 Mon Sep 17 00:00:00 2001 From: openoms Date: Thu, 31 Aug 2023 11:32:59 +0200 Subject: [PATCH 14/56] build: git verify the last commit instead of tag --- build_joininbox.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_joininbox.sh b/build_joininbox.sh index 9750546..267e138 100644 --- a/build_joininbox.sh +++ b/build_joininbox.sh @@ -409,7 +409,7 @@ else fi command="sudo -u joinmarket bash /home/joinmarket/joininbox/scripts/verify.git.sh \ - ${PGPsigner} ${PGPpubkeyLink} ${PGPpubkeyFingerprint} ${tag}" + ${PGPsigner} ${PGPpubkeyLink} ${PGPpubkeyFingerprint}" echo "running: ${command}" chmod 777 /dev/shm ${command} || exit 1 From c240ba2885cc6b6207387cdadf18ac09322935f0 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:28:35 +0200 Subject: [PATCH 15/56] build: update to Debian 12.2.0, create home folders as 0755 (#139) * build: create home folders as 0755 on Debian 12 * build: add bash * fix: add --shell /bin/bash to user creation * copy the skeleton files for users with login * update workflows * jam-remote update to v0.1.6 * amd64 image update to debian-12.2.0, clean vars --- .github/workflows/amd64-image-build.yml | 11 +++++++++-- .github/workflows/arm64-rpi-image-build.yml | 11 +++++++++-- build_joininbox.sh | 8 +++++--- ci/amd64/debian/joininbox-amd64-debian.json | 12 ++---------- ci/amd64/packer.build.amd64-debian.sh | 21 +++++++++++++-------- scripts/install.joinmarket.sh | 4 +++- scripts/jam-remote/install.jam.sh | 19 ++++++++++++------- scripts/standalone/_functions.standalone.sh | 10 +++++++--- scripts/standalone/install.specter.sh | 2 +- 9 files changed, 61 insertions(+), 37 deletions(-) diff --git a/.github/workflows/amd64-image-build.yml b/.github/workflows/amd64-image-build.yml index f8ff37a..f0524bc 100644 --- a/.github/workflows/amd64-image-build.yml +++ b/.github/workflows/amd64-image-build.yml @@ -26,17 +26,24 @@ jobs: - uses: actions/checkout@v3 - name: Set values + id: set_values run: | echo "BUILD_DATE=$(date +"%Y-%m-%d")" >> $GITHUB_ENV echo "BUILD_VERSION=$(git describe --always --tags)" >> $GITHUB_ENV + if [[ "${{github.event_name}}" == "pull_request" ]]; then + echo "::set-output name=github_user::${{github.event.pull_request.head.repo.owner.login}}" + else + echo "::set-output name=github_user::$(echo ${{github.repository}} | cut -d'/' -f1)" + fi - - name: Hello JoininBox + - name: Display the build name run: echo "Building the joininbox-amd64-debian-image-${{ env.BUILD_DATE }}-${{ env.BUILD_VERSION }}" - name: Run the build script run: | + echo "Running with: ${{steps.set_values.outputs.github_user}} $GITHUB_HEAD_REF" cd ci/amd64 - bash packer.build.amd64-debian.sh $GITHUB_ACTOR $GITHUB_HEAD_REF + bash packer.build.amd64-debian.sh ${{steps.set_values.outputs.github_user}} $GITHUB_HEAD_REF - name: Compute checksum of the raw image run: | diff --git a/.github/workflows/arm64-rpi-image-build.yml b/.github/workflows/arm64-rpi-image-build.yml index 6d2c1f5..66640fb 100644 --- a/.github/workflows/arm64-rpi-image-build.yml +++ b/.github/workflows/arm64-rpi-image-build.yml @@ -28,11 +28,17 @@ jobs: - uses: actions/checkout@v3 - name: Set values + id: set_values run: | echo "BUILD_DATE=$(date +"%Y-%m-%d")" >> $GITHUB_ENV echo "BUILD_VERSION=$(git describe --always --tags)" >> $GITHUB_ENV + if [[ "${{github.event_name}}" == "pull_request" ]]; then + echo "::set-output name=github_user::${{github.event.pull_request.head.repo.owner.login}}" + else + echo "::set-output name=github_user::$(echo ${{github.repository}} | cut -d'/' -f1)" + fi - - name: Hello JoininBox + - name: Display the build name run: echo "Building the joininbox-arm64-rpi-image-${{ env.BUILD_DATE }}-${{ env.BUILD_VERSION }}" - name: Set up QEMU @@ -42,8 +48,9 @@ jobs: - name: Run the build script run: | + echo "Running with: ${{steps.set_values.outputs.github_user}} $GITHUB_HEAD_REF" cd ci/arm64-rpi - bash arm64-rpi.sh $GITHUB_ACTOR $GITHUB_HEAD_REF + bash arm64-rpi.sh ${{steps.set_values.outputs.github_user}} $GITHUB_HEAD_REF - name: Compute checksum of the raw image run: | diff --git a/build_joininbox.sh b/build_joininbox.sh index 267e138..3029fb0 100644 --- a/build_joininbox.sh +++ b/build_joininbox.sh @@ -368,7 +368,9 @@ echo "# JoininBox" echo "#############" echo echo "# add the 'joinmarket' user" -adduser --disabled-password --gecos "" joinmarket +adduser --system --group --shell /bin/bash --home /home/joinmarket joinmarket +echo "Copy the skeleton files for login" +sudo -u joinmarket cp -r /etc/skel/. /home/joinmarket/ echo "# clone the joininbox repo and copy the scripts" cd /home/joinmarket || exit 1 @@ -607,7 +609,7 @@ echo "#########################" echo "# Download Bitcoin Core" echo "#########################" echo -sudo -u joinmarket /home/joinmarket/install.bitcoincore.sh downloadCoreOnly || exit 1 +sudo -u joinmarket bash /home/joinmarket/install.bitcoincore.sh downloadCoreOnly || exit 1 echo echo "######################" @@ -623,7 +625,7 @@ if [ "$4" = "without-qt" ]; then qtgui="false" sed -i "s/^qtgui=.*/qtgui=false/g" /home/joinmarket/joinin.conf fi -sudo -u joinmarket /home/joinmarket/install.joinmarket.sh -i install -q $qtgui || exit 1 +sudo -u joinmarket bash /home/joinmarket/install.joinmarket.sh -i install -q $qtgui || exit 1 echo "###################" echo "# bootstrap.service" diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index 539f784..f4fb581 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -1,23 +1,17 @@ { "variables": { - "iso_name": "debian-12.1.0-amd64-netinst.iso", - "iso_checksum": "9f181ae12b25840a508786b1756c6352a0e58484998669288c4eec2ab16b8559", + "iso_name": "debian-12.2.0-amd64-netinst.iso", + "iso_checksum": "23ab444503069d9ef681e3028016250289a33cc7bab079259b73100daee0af66", "box_basename": "debian", "build_directory": "../builds", - "build_timestamp": "{{isotime \"20060102150405\"}}", "cpus": "2", "disk_size": "30000", - "git_revision": "__unknown_git_revision__", - "guest_additions_url": "", "headless": "false", "http_directory": "{{template_dir}}/http", - "http_proxy": "{{env `http_proxy`}}", - "https_proxy": "{{env `https_proxy`}}", "memory": "2048", "mirror": "http://cdimage.debian.org/cdimage/release", "mirror_directory": "current/amd64/iso-cd", "name": "debian", - "no_proxy": "{{env `no_proxy`}}", "preseed_path": "debian-9/preseed.cfg", "qemu_display": "none", "qemu_bios": "bios-256k.bin", @@ -56,8 +50,6 @@ "boot_wait": "5s", "cpus": "{{ user `cpus` }}", "disk_size": "{{user `disk_size`}}", - "guest_additions_path": "VBoxGuestAdditions_{{.Version}}.iso", - "guest_additions_url": "{{ user `guest_additions_url` }}", "guest_os_type": "Debian_64", "hard_drive_interface": "sata", "headless": "{{ user `headless` }}", diff --git a/ci/amd64/packer.build.amd64-debian.sh b/ci/amd64/packer.build.amd64-debian.sh index 6f6925a..2cded91 100644 --- a/ci/amd64/packer.build.amd64-debian.sh +++ b/ci/amd64/packer.build.amd64-debian.sh @@ -1,14 +1,19 @@ #!/bin/bash -e -# Install packer -curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - -sudo apt-add-repository "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 +# 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 "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" +fi # Install qemu -echo -e "\nInstalling qemu..." +echo "# Install qemu ..." +sudo apt-get update sudo apt-get install -y qemu-system if [ $# -gt 0 ]; then @@ -24,7 +29,7 @@ else fi # Build the image -echo -e "\nBuilding image..." +echo "# Building image ..." cd debian PACKER_LOG=1 packer build \ -var github_user=${github_user} -var branch=${branch} \ diff --git a/scripts/install.joinmarket.sh b/scripts/install.joinmarket.sh index 5de3248..2251308 100755 --- a/scripts/install.joinmarket.sh +++ b/scripts/install.joinmarket.sh @@ -123,7 +123,9 @@ source /home/joinmarket/joinin.conf # create user if not default if [ "${user}" != "joinmarket" ]; then echo "# add the '${user}' user" - sudo adduser --disabled-password --gecos "" ${user} + sudo adduser --system --group --shell /bin/bash --home /home/${user} ${user} + echo "Copy the skeleton files for login" + sudo -u ${user} cp -r /etc/skel/. /home/${user}/ sudo adduser ${user} sudo # add user to Tor group sudo usermod -a -G debian-tor ${user} diff --git a/scripts/jam-remote/install.jam.sh b/scripts/jam-remote/install.jam.sh index 10c67e2..5fe7958 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=0.1.5 +WEBUI_VERSION="v0.1.6" REPO=joinmarket-webui/jam HOME_DIR=/home/${USERNAME} APP_DIR=webui @@ -46,7 +46,7 @@ if [ "$1" = "on" ]; then echo "# Creating the ${USERNAME} user" echo - sudo adduser --disabled-password --gecos "" ${USERNAME} + sudo adduser --system --group --home /home/${USERNAME} ${USERNAME} # install nodeJS bash ${SOURCEDIR}/bonus.nodejs.sh on @@ -57,10 +57,10 @@ if [ "$1" = "on" ]; then sudo -u $USERNAME git clone https://github.com/$REPO cd jam || exit 1 - sudo -u $USERNAME git reset --hard v${WEBUI_VERSION} + sudo -u $USERNAME git reset --hard ${WEBUI_VERSION} - sudo -u $USERNAME bash ${SOURCEDIR}/../verify.git.sh \ - "${PGPsigner}" "${PGPpubkeyLink}" "${PGPpubkeyFingerprint}" "v${WEBUI_VERSION}" || exit 1 + #sudo -u $USERNAME bash ${SOURCEDIR}/../verify.git.sh \ + # "${PGPsigner}" "${PGPpubkeyLink}" "${PGPpubkeyFingerprint}" "v${WEBUI_VERSION}" || exit 1 cd $HOME_DIR || exit 1 sudo -u $USERNAME mv jam $APP_DIR @@ -134,10 +134,15 @@ if [ "$1" = "update" ]; then echo "FAIL - npm install did not run correctly, aborting" exit 1 fi - - sudo -u $USERNAME npm run build echo "*** JAM UPDATED to $version ***" fi + + if ! sudo -u $USERNAME npm install; then + echo "FAIL - npm install did not run correctly, aborting" + exit 1 + fi + sudo -u $USERNAME npm run build + else echo "*** JAM NOT INSTALLED ***" fi diff --git a/scripts/standalone/_functions.standalone.sh b/scripts/standalone/_functions.standalone.sh index 42939e5..32f909a 100755 --- a/scripts/standalone/_functions.standalone.sh +++ b/scripts/standalone/_functions.standalone.sh @@ -3,7 +3,7 @@ function addUserStore() { if [ ! -d /home/store/app-data ]; then echo "# Adding the user: store" - sudo adduser --disabled-password --gecos "" store + sudo adduser --system --group --home /home/store store sudo -u store mkdir /home/store/app-data echo "# Add the joinmarket user to the store group" sudo usermod -aG store joinmarket @@ -120,7 +120,9 @@ function downloadSnapShot() { sudo mkdir -p /home/store/app-data/.bitcoin fi echo "# Making sure user: bitcoin exists" - sudo adduser --disabled-password --gecos "" bitcoin + sudo adduser --system --group --shell /bin/bash --home /home/bitcoin bitcoin + echo "Copy the skeleton files for login" + sudo -u bitcoin cp -r /etc/skel/. /home/bitcoin/ sudo chown -R bitcoin:bitcoin /home/store/app-data/.bitcoin echo "# Add the joinmarket user to the bitcoin group" sudo usermod -aG bitcoin joinmarket @@ -154,7 +156,9 @@ function installBitcoinCoreStandalone() { echo "${installedVersion} is already installed" else echo "# Adding the user: bitcoin" - sudo adduser --disabled-password --gecos "" bitcoin + sudo adduser --system --group --shell /bin/bash --home /home/bitcoin bitcoin + echo "Copy the skeleton files for login" + sudo -u bitcoin cp -r /etc/skel/. /home/bitcoin/ echo "# Add the joinmarket user to the bitcoin group" sudo usermod -aG bitcoin joinmarket echo "# Installing Bitcoin Core v${bitcoinVersion}" diff --git a/scripts/standalone/install.specter.sh b/scripts/standalone/install.specter.sh index dfb21eb..954ecc3 100755 --- a/scripts/standalone/install.specter.sh +++ b/scripts/standalone/install.specter.sh @@ -149,7 +149,7 @@ if [ "$1" = "1" ] || [ "$1" = "on" ]; then addUserStore - sudo adduser --disabled-password --gecos "" specter + sudo adduser --system --group --home /home/specter specter # store data with the store user sudo mkdir -p /home/store/app-data/.specter 2>/dev/null From 8f61362f9b43b1badcd6c08ea350fa3815391a51 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 11 Dec 2023 08:50:42 +0100 Subject: [PATCH 16/56] Bitcoin Core update to v26.0 , add update option and display (#140) * add aliases bitcoinlog, bitcoinconf * add bitcoin.update.sh * add update option, display bitcoin core version * bitcoin core update to v26.0 * add deprecatedrpc=create_bdb, update services fixes #141 * ci: update arm64-rpi image * ci: install qemu plugin for packer * ci: add vnc * menu updates --- .gitignore | 1 + ci/amd64/debian/joininbox-amd64-debian.json | 114 ++-------- ci/amd64/packer.build.amd64-debian.sh | 5 +- ci/arm64-rpi/arm64-rpi.pkr.hcl | 4 +- scripts/_functions.bitcoincore.sh | 57 ++++- scripts/_functions.sh | 1 + scripts/menu.update.advanced.sh | 9 +- scripts/menu.update.sh | 26 ++- scripts/standalone/_functions.standalone.sh | 46 +++- scripts/standalone/bitcoin.update.sh | 219 ++++++++++++++++++++ 10 files changed, 359 insertions(+), 123 deletions(-) create mode 100644 scripts/standalone/bitcoin.update.sh diff --git a/.gitignore b/.gitignore index 025a3fe..0329ccb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ scripts/wallets ci/arm64-rpi/.packer* ci/arm64-rpi/joininbox-arm64-rpi* +ci/amd64/builds diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index f4fb581..817f324 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -1,118 +1,46 @@ { "variables": { - "iso_name": "debian-12.2.0-amd64-netinst.iso", - "iso_checksum": "23ab444503069d9ef681e3028016250289a33cc7bab079259b73100daee0af66", - "box_basename": "debian", - "build_directory": "../builds", - "cpus": "2", - "disk_size": "30000", - "headless": "false", - "http_directory": "{{template_dir}}/http", - "memory": "2048", - "mirror": "http://cdimage.debian.org/cdimage/release", - "mirror_directory": "current/amd64/iso-cd", - "name": "debian", - "preseed_path": "debian-9/preseed.cfg", - "qemu_display": "none", - "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 ", - "version": "TIMESTAMP" -}, + "iso_name": "debian-12.2.0-amd64-netinst.iso", + "iso_checksum": "23ab444503069d9ef681e3028016250289a33cc7bab079259b73100daee0af66", + "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": "{{ user `cpus` }}", - "disk_size": "{{user `disk_size`}}", + "cpus": "2", + "disk_size": "30000", "headless": "{{ user `headless` }}", "http_directory": "{{user `http_directory`}}", "iso_checksum": "{{user `iso_checksum`}}", - "iso_url": "{{user `mirror`}}/{{user `mirror_directory`}}/{{user `iso_name`}}", + "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_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", "{{ user `qemu_display` }}" ] + [ "-display", "none" ] ] - }, - { - "boot_command": "{{user `boot_command`}}", - "boot_wait": "5s", - "cpus": "{{ user `cpus` }}", - "disk_size": "{{user `disk_size`}}", - "guest_os_type": "Debian_64", - "hard_drive_interface": "sata", - "headless": "{{ user `headless` }}", - "http_directory": "{{user `http_directory`}}", - "iso_checksum": "{{user `iso_checksum`}}", - "iso_url": "{{user `mirror`}}/{{user `mirror_directory`}}/{{user `iso_name`}}", - "memory": "{{ user `memory` }}", - "output_directory": "{{ user `build_directory` }}/packer-{{user `template`}}-virtualbox", - "shutdown_command": "echo 'joininbox' | sudo -S /sbin/shutdown -hP now", - "ssh_password": "joininbox", - "ssh_port": 22, - "ssh_timeout": "10000s", - "ssh_username": "joinmarket", - "type": "virtualbox-iso", - "virtualbox_version_file": ".vbox_version", - "vm_name": "{{ user `template` }}" - }, - { - "boot_command": "{{user `boot_command`}}", - "boot_wait": "5s", - "cpus": "{{ user `cpus` }}", - "disk_size": "{{user `disk_size`}}", - "guest_os_type": "debian8-64", - "headless": "{{ user `headless` }}", - "http_directory": "{{user `http_directory`}}", - "iso_checksum": "{{user `iso_checksum`}}", - "iso_url": "{{user `mirror`}}/{{user `mirror_directory`}}/{{user `iso_name`}}", - "memory": "{{ user `memory` }}", - "output_directory": "{{ user `build_directory` }}/packer-{{user `template`}}-vmware", - "shutdown_command": "echo 'joininbox' | sudo -S /sbin/shutdown -hP now", - "ssh_password": "joininbox", - "ssh_port": 22, - "ssh_timeout": "10000s", - "ssh_username": "joinmarket", - "tools_upload_flavor": "linux", - "type": "vmware-iso", - "vm_name": "{{ user `template` }}", - "vmx_data": { - "cpuid.coresPerSocket": "1", - "ethernet0.pciSlotNumber": "32" - }, - "vmx_remove_ethernet_interfaces": true - }, - { - "boot_command": "{{user `boot_command`}}", - "boot_wait": "5s", - "cpus": "{{ user `cpus` }}", - "disk_size": "{{user `disk_size`}}", - "guest_os_type": "debian", - "http_directory": "{{user `http_directory`}}", - "iso_checksum": "{{user `iso_checksum`}}", - "iso_url": "{{user `mirror`}}/{{user `mirror_directory`}}/{{user `iso_name`}}", - "memory": "{{ user `memory` }}", - "output_directory": "{{ user `build_directory` }}/packer-{{user `template`}}-parallels", - "parallels_tools_flavor": "lin", - "prlctl_version_file": ".prlctl_version", - "shutdown_command": "echo 'joininbox' | sudo -S /sbin/shutdown -hP now", - "ssh_password": "joininbox", - "ssh_port": 22, - "ssh_timeout": "10000s", - "ssh_username": "joinmarket", - "type": "parallels-iso", - "vm_name": "{{ user `template` }}" } ], "provisioners": [ diff --git a/ci/amd64/packer.build.amd64-debian.sh b/ci/amd64/packer.build.amd64-debian.sh index 2cded91..8da4c62 100644 --- a/ci/amd64/packer.build.amd64-debian.sh +++ b/ci/amd64/packer.build.amd64-debian.sh @@ -11,11 +11,14 @@ else echo "# Packer is installed" fi -# Install qemu +# install qemu echo "# Install qemu ..." sudo apt-get update sudo apt-get install -y qemu-system +# install qemu plugin +packer plugins install github.com/hashicorp/qemu + if [ $# -gt 0 ]; then github_user=$1 else diff --git a/ci/arm64-rpi/arm64-rpi.pkr.hcl b/ci/arm64-rpi/arm64-rpi.pkr.hcl index 4f7c294..7592b1b 100644 --- a/ci/arm64-rpi/arm64-rpi.pkr.hcl +++ b/ci/arm64-rpi/arm64-rpi.pkr.hcl @@ -3,10 +3,10 @@ variable "branch" {} source "arm" "joininbox-arm64-rpi" { file_checksum_type = "sha256" - file_checksum = "a68cd2bfe7831c438d8a5d832803ae0db17afec9f3cd370d9e8748c7b5456283" + file_checksum = "ace82f0d5e3586036cb72c5e46eb6222e93365503c51385496e3e2ee9999a5ad" file_target_extension = "xz" file_unarchive_cmd = ["xz", "--decompress", "$ARCHIVE_PATH"] - file_urls = ["https://raspi.debian.net/tested/20230612_raspi_4_bookworm.img.xz"] + file_urls = ["https://raspi.debian.net/tested/20231109_raspi_4_bookworm.img.xz"] image_build_method = "resize" image_chroot_env = ["PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin"] image_partitions { diff --git a/scripts/_functions.bitcoincore.sh b/scripts/_functions.bitcoincore.sh index 5ab5b5c..3952d4d 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="25.0" + bitcoinVersion="26.0" if bitcoin-cli --version | grep $bitcoinVersion >/dev/null; then echo "# Bitcoin Core $bitcoinVersion is already installed" @@ -39,7 +39,7 @@ function downloadBitcoinCore() { echo else echo - echo "# BUILD FAILED --> the PGP verification failed / signature(${goodSignature}) " + echo "# BUILD FAILED --> the PGP verification failed" exit 1 fi @@ -155,25 +155,46 @@ function installSignet() { Description=Bitcoin daemon on signet [Service] +Environment='MALLOC_ARENA_MAX=1' +PIDFile=/home/joinmarket/bitcoin/bitcoind.pid +ExecStart=/home/joinmarket/bitcoin/bitcoind -signet -daemon \\ + -datadir=/home/joinmarket/.bitcoin \\ + -conf=/home/joinmarket/.bitcoin/bitcoin.conf \\ + -pid=/home/joinmarket/bitcoin/bitcoind.pid +PermissionsStartOnly=true + +# Process management +#################### +Type=forking +Restart=on-failure +TimeoutStartSec=infinity +TimeoutStopSec=600 + +# Directory creation and permissions +#################################### +# Run as joinmarket:joinmarket User=joinmarket Group=joinmarket -Type=forking -PIDFile=/home/joinmarket/bitcoin/bitcoind.pid -ExecStart=/home/joinmarket/bitcoin/bitcoind -signet -daemon \ - -datadir=/home/joinmarket/.bitcoin \ - -conf=/home/joinmarket/.bitcoin/bitcoin.conf \ - -pid=/home/joinmarket/bitcoin/bitcoind.pid -Restart=always -TimeoutSec=120 -RestartSec=30 + StandardOutput=null StandardError=journal # Hardening measures +#################### +# Provide a private /tmp and /var/tmp. PrivateTmp=true +# Mount /usr, /boot/ and /etc read-only for the process. ProtectSystem=full +# Deny access to /home, /root and /run/user +ProtectHome=true +# Disallow the process and all of its children to gain +# new privileges through execve(). NoNewPrivileges=true +# Use a new /dev namespace only populated with API pseudo devices +# such as /dev/null, /dev/zero and /dev/random. PrivateDevices=true +# Deny the creation of writable and executable memory mappings. +MemoryDenyWriteExecute=true [Install] WantedBy=multi-user.target @@ -310,6 +331,20 @@ function checkRPCwallet { 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 + bitcoinConfPath="/mnt/hdd/bitcoin/bitcoin.conf" + 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/) diff --git a/scripts/_functions.sh b/scripts/_functions.sh index c7541b6..fc68e64 100755 --- a/scripts/_functions.sh +++ b/scripts/_functions.sh @@ -7,6 +7,7 @@ currentJBcommit=$(cd /home/joinmarket/joininbox; git describe --tags) currentJBtag=$(cd /home/joinmarket/joininbox; git tag | sort -V | tail -1) currentJMversion=$(cd /home/joinmarket/joinmarket-clientserver 2>/dev/null; \ git describe --tags 2>/dev/null) +currentBTCversion=$(bitcoind --version | grep version) # paths walletPath="/home/joinmarket/.joinmarket/wallets/" diff --git a/scripts/menu.update.advanced.sh b/scripts/menu.update.advanced.sh index b429c7e..7b051db 100755 --- a/scripts/menu.update.advanced.sh +++ b/scripts/menu.update.advanced.sh @@ -3,13 +3,16 @@ source /home/joinmarket/_functions.sh # BASIC MENU INFO -HEIGHT=16 +HEIGHT=18 WIDTH=60 CHOICE_HEIGHT=7 TITLE="Advanced update options" MENU=" -Current JoininBox version: $currentJBcommit -Current JoinMarket version: $currentJMversion" +Installed versions: +JoininBox $currentJBcommit +JoinMarket $currentJMversion +$currentBTCversion" + OPTIONS=() BACKTITLE="JoininBox GUI" diff --git a/scripts/menu.update.sh b/scripts/menu.update.sh index 071f0ce..336aca3 100755 --- a/scripts/menu.update.sh +++ b/scripts/menu.update.sh @@ -3,20 +3,28 @@ source /home/joinmarket/_functions.sh # BASIC MENU INFO -HEIGHT=12 +HEIGHT=15 WIDTH=56 -CHOICE_HEIGHT=3 +CHOICE_HEIGHT=4 TITLE="Update options" MENU=" -Current JoininBox version: $currentJBcommit -Current JoinMarket version: $currentJMversion" +Installed versions: +JoininBox $currentJBcommit +JoinMarket $currentJMversion +$currentBTCversion" OPTIONS=() BACKTITLE="JoininBox GUI" # Basic Options 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 $(grep testedJMversion= < ~/install.joinmarket.sh | cut -d '"' -f 2)") + +if [ "$runningEnv" = "standalone" ]; then + OPTIONS+=(\ + BITCOIN "Update Bitcoin Core to a chosen version") +fi +OPTIONS+=(\ ADVANCED "Advanced update options") CHOICE=$(dialog --clear \ @@ -47,6 +55,14 @@ case $CHOICE in echo echo "Press ENTER to return to the menu" read key;; + BITCOIN) + /home/joinmarket/standalone/bitcoin.update.sh custom + errorOnInstall $? + echo + echo "# Start bitcoind .. " + sudo systemctl start bitcoind + echo "# Monitoring the bitcoind logs .. " + showBitcoinLogs;; ADVANCED) /home/joinmarket/menu.update.advanced.sh;; esac diff --git a/scripts/standalone/_functions.standalone.sh b/scripts/standalone/_functions.standalone.sh index 32f909a..d80d2eb 100755 --- a/scripts/standalone/_functions.standalone.sh +++ b/scripts/standalone/_functions.standalone.sh @@ -220,24 +220,46 @@ function installMainnet() { echo " [Unit] Description=Bitcoin daemon on mainnet + [Service] +Environment='MALLOC_ARENA_MAX=1' +PIDFile=/home/bitcoin/bitcoin/bitcoind.pid +ExecStart=/home/bitcoin/bitcoin/bitcoind -daemon \\ + -pid=/home/bitcoin/bitcoin/bitcoind.pid +PermissionsStartOnly=true + +# Process management +#################### +Type=forking +Restart=on-failure +TimeoutStartSec=infinity +TimeoutStopSec=600 + +# Directory creation and permissions +#################################### +# Run as bitcoin:bitcoin User=bitcoin Group=bitcoin -Type=forking -PIDFile=/home/bitcoin/bitcoin/bitcoind.pid -ExecStart=/home/bitcoin/bitcoin/bitcoind -daemon \ --pid=/home/bitcoin/bitcoin/bitcoind.pid -Restart=always -TimeoutSec=120 -RestartSec=30 + StandardOutput=null StandardError=journal # Hardening measures +#################### +# Provide a private /tmp and /var/tmp. PrivateTmp=true +# Mount /usr, /boot/ and /etc read-only for the process. ProtectSystem=full +# Deny access to /home, /root and /run/user +ProtectHome=true +# Disallow the process and all of its children to gain +# new privileges through execve(). NoNewPrivileges=true +# Use a new /dev namespace only populated with API pseudo devices +# such as /dev/null, /dev/zero and /dev/random. PrivateDevices=true +# Deny the creation of writable and executable memory mappings. +MemoryDenyWriteExecute=true [Install] WantedBy=multi-user.target @@ -247,10 +269,18 @@ WantedBy=multi-user.target echo "# OK - the bitcoind.service is now enabled" # add aliases - if [ $(alias | grep -c "sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli") -eq 0 ]; then + if ! grep "alias bitcoin-cli" /home/joinmarket/_aliases.sh; then sudo bash -c "echo 'alias bitcoin-cli=\"sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli\"' >> /home/joinmarket/_aliases.sh" + fi + if ! grep "alias bitcoind" /home/joinmarket/_aliases.sh; then sudo bash -c "echo 'alias bitcoind=\"sudo -u bitcoin /home/bitcoin/bitcoin/bitcoind\"' >> /home/joinmarket/_aliases.sh" fi + if ! grep "alias bitcoinlog" /home/joinmarket/_aliases.sh; then + sudo bash -c "echo 'alias bitcoinlog=\"sudo tail -f /home/bitcoin/.bitcoin/debug.log\"' >> /home/joinmarket/_aliases.sh" + fi + if ! grep "alias bitcoinconf" /home/joinmarket/_aliases.sh; then + sudo bash -c "echo 'alias bitcoinconf=\"sudo nano /home/bitcoin/.bitcoin/bitcoin.conf\"' >> /home/joinmarket/_aliases.sh" + fi # set joinin.conf value /home/joinmarket/set.value.sh set network mainnet ${joininConfPath} diff --git a/scripts/standalone/bitcoin.update.sh b/scripts/standalone/bitcoin.update.sh new file mode 100644 index 0000000..d5ffe4d --- /dev/null +++ b/scripts/standalone/bitcoin.update.sh @@ -0,0 +1,219 @@ +#!/bin/bash + +# command info +if [ $# -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "-help" ]; then + echo "Interim optional Bitcoin Core updates." + echo "bitcoin.update.sh [info|tested|reckless|custom]" + echo "info -> get actual state and possible actions" + echo "tested -> only do a tested update by the RaspiBlitz team" + echo "reckless -> the update was not tested by the RaspiBlitz team" + echo "custom -> update to a chosen version" + echo " the binary checksum and signatures will be checked in all cases" + echo " except when 'skipverify' is used" + echo + exit 1 +fi + +echo "# Running: bitcoin.update.sh $*" + +# 1. parameter [info|tested|reckless] +mode="$1" + +# RECOMMENDED UPDATE BY RASPIBLITZ TEAM (just possible once per sd card update) +# comment will be shown as "BEWARE Info" when option is chosen (can be multiple lines) +bitcoinVersion="" # example: 22.0 .. keep empty if no newer version as sd card build is available + +# GATHER DATA +# setting download directory to the current user +downloadDir="/home/$(whoami)/download/bitcoin.update" + +# bitcoinOSversion +if [ "$(uname -m | grep -c 'arm')" -gt 0 ]; then + bitcoinOSversion="arm-linux-gnueabihf" +elif [ "$(uname -m | grep -c 'aarch64')" -gt 0 ]; then + bitcoinOSversion="aarch64-linux-gnu" +elif [ "$(uname -m | grep -c 'x86_64')" -gt 0 ]; then + bitcoinOSversion="x86_64-linux-gnu" +fi + +# installed version +installedVersion=$(sudo -u bitcoin /home/bitcoin/bitcoin/bitcoind --version | head -n1 | cut -d" " -f4 | cut -c 2-) + +# test if the installed version already the tested/recommended update version +bitcoinUpdateInstalled=$(echo "${installedVersion}" | grep -c "${bitcoinVersion}") + +# get latest release from GitHub releases +bitcoinLatestVersion=$(curl --header "X-GitHub-Api-Version:2022-11-28" -s https://api.github.com/repos/bitcoin/bitcoin/releases | jq -r '.[].tag_name' | sort | tail -n1 | cut -c 2-) + +# INFO +function displayInfo() { + echo "# basic data" + echo "installedVersion='${installedVersion}'" + echo "bitcoinOSversion='${bitcoinOSversion}'" + + echo "# the tested/recommended update option" + echo "bitcoinUpdateInstalled='${bitcoinUpdateInstalled}'" + echo "bitcoinVersion='${bitcoinVersion}'" + + echo "# reckless update option (latest Bitcoin Core release from GitHub)" + echo "bitcoinLatestVersion='${bitcoinLatestVersion}'" +} + +if [ "${mode}" = "info" ]; then + displayInfo + exit 1 +fi + +# tested +if [ "${mode}" = "tested" ]; then + + echo "# bitcoin.update.sh tested" + +elif [ "${mode}" = "reckless" ]; then + # RECKLESS + # this mode is just for people running test and development nodes - its not recommended + # for production nodes. In a update/recovery scenario it will not install a fixed version + # it will always pick the latest release from the github + echo "# bitcoin.update.sh reckless" + bitcoinVersion=${bitcoinLatestVersion} + pathVersion=${bitcoinVersion} + +elif [ "${mode}" = "custom" ]; then + if [ $# -gt 1 ]; then + bitcoinVersion="$2" + else + clear + echo + echo "# Update Bitcoin Core to a chosen version." + echo + echo "# Input the version you would like to install and press ENTER." + echo "# Examples (versions below 22.1 are not supported):" + echo "24.0.1" + echo "26.0" + echo + read bitcoinVersion + fi + + if [ $(echo ${bitcoinVersion} | grep -c "rc") -gt 0 ]; then + cutVersion=$(echo ${bitcoinVersion} | awk -F"r" '{print $1}') + rcVersion=$(echo ${bitcoinVersion} | awk -F"r" '{print $2}') + # https://bitcoincore.org/bin/bitcoin-core-22.0/test.rc3/ + pathVersion=${cutVersion}/test.r${rcVersion} + else + pathVersion=${bitcoinVersion} + fi + + if curl --output /dev/null --silent --head --fail \ + https://bitcoincore.org/bin/bitcoin-core-${pathVersion}/SHA256SUMS.asc; then + echo "# OK version exists at https://bitcoincore.org/bin/bitcoin-core-${pathVersion}" + if [ "${mode}" = "custom" ] && [ "$3" = "skipverify" ]; then + echo "# skipping signature verification" + fi + echo "# Press ENTER to proceed to install Bitcoin Core $bitcoinVersion or CTRL+C to abort." + read key + else + echo "# FAIL $bitcoinVersion does not exist" + echo + echo "# Press ENTER to return to the main menu" + read key + exit 0 + fi +fi + +# JOINED INSTALL +if [ "${mode}" = "tested" ] || [ "${mode}" = "reckless" ] || [ "${mode}" = "custom" ]; then + + displayInfo + + if [ "$installedVersion" = "$bitcoinVersion" ]; then + echo "# installedVersion = bitcoinVersion" + echo "# exiting script" + exit 0 + fi + + echo + echo "# clean & change into download directory" + sudo rm -rf "${downloadDir}" + mkdir -p "${downloadDir}" + cd "${downloadDir}" || exit 1 + + echo "# Receive signer keys" + curl -s "https://api.github.com/repos/bitcoin-core/guix.sigs/contents/builder-keys" | + jq -r '.[].download_url' | while read url; do curl -s "$url" | gpg --import; done + + # download signed binary sha256 hash sum file + wget --prefer-family=ipv4 --progress=bar:force -O SHA256SUMS https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/SHA256SUMS + # download the signed binary sha256 hash sum file and check + wget --prefer-family=ipv4 --progress=bar:force -O SHA256SUMS.asc https://bitcoincore.org/bin/bitcoin-core-${bitcoinVersion}/SHA256SUMS.asc + + if [ "${mode}" = "custom" ] && [ "$3" = "skipverify" ]; then + echo "# skipping signature verification" + echo "# display the output of 'gpg --verify SHA256SUMS.asc'" + gpg --verify SHA256SUMS.asc + else + if gpg --verify SHA256SUMS.asc; then + echo + echo "****************************************" + echo "OK --> BITCOIN MANIFEST IS CORRECT" + echo "****************************************" + echo + else + echo + echo "# BUILD FAILED --> the PGP verification failed" + echo "# try again or with a different version" + echo "# if you want to skip verifying all signatures (and just show them) use the command:" + echo "# /home/admin/config.scripts/bonus.bitcoin.sh custom ${bitcoinVersion:-} skipverify" + exit 1 + fi + fi + + echo "# Downloading Bitcoin Core v${bitcoinVersion} for ${bitcoinOSversion} ..." + binaryName="bitcoin-${bitcoinVersion}-${bitcoinOSversion}.tar.gz" + wget https://bitcoincore.org/bin/bitcoin-core-${pathVersion}/${binaryName} + if [ ! -f "./${binaryName}" ]; then + echo "# FAIL # Downloading BITCOIN BINARY did not succeed." + exit 1 + fi + + echo "# Checking the binary checksum ..." + if ! sha256sum -c --ignore-missing SHA256SUMS; then + # get the sha256 value for the corresponding platform from signed hash sum file + bitcoinSHA256=$(grep -i "${binaryName}}" SHA256SUMS | cut -d " " -f1) + echo "# FAIL # Downloaded BITCOIN BINARY CHECKSUM:" + echo "$(sha256sum ${binaryName})" + echo "NOT matching SHA256 checksum:" + echo "${bitcoinSHA256}" + exit 1 + else + echo + echo "# OK --> VERIFIED BITCOIN CORE BINARY CHECKSUM IS CORRECT" + echo + fi +fi + +# JOINED INSTALL +if [ "${mode}" = "tested" ] || [ "${mode}" = "reckless" ] || [ "${mode}" = "custom" ]; then + + # install + echo "# Stopping bitcoind ..." + sudo systemctl stop bitcoind 2>/dev/null + sudo systemctl stop tbitcoind 2>/dev/null + sudo systemctl stop sbitcoind 2>/dev/null + echo + echo "# Installing Bitcoin Core v${bitcoinVersion}" + tar -xvf ${binaryName} + sudo install -m 0755 -o root -g root -t /home/bitcoin/bitcoin/ bitcoin-${bitcoinVersion}/bin/* + sleep 3 + if ! sudo /home/bitcoin/bitcoin/bitcoind --version | grep "${bitcoinVersion}"; then + echo + echo "# BUILD FAILED --> Was not able to install bitcoind version(${bitcoinVersion})" + exit 1 + fi + + echo "# OK Bitcoin Core ${bitcoinVersion} is installed" + exit 0 + +else + echo "# error='parameter not known'" + exit 1 +fi From 7b2166d124eb4f26c9027eae2667cdf35f5f354a Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 11 Dec 2023 11:00:37 +0100 Subject: [PATCH 17/56] build: debian update to 12.4.0-amd64 (#142) --- ci/amd64/debian/joininbox-amd64-debian.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index 817f324..718117e 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -1,7 +1,7 @@ { "variables": { - "iso_name": "debian-12.2.0-amd64-netinst.iso", - "iso_checksum": "23ab444503069d9ef681e3028016250289a33cc7bab079259b73100daee0af66", + "iso_name": "debian-12.4.0-amd64-netinst.iso", + "iso_checksum": "64d727dd5785ae5fcfd3ae8ffbede5f40cca96f1580aaa2820e8b99dae989d94", "box_basename": "debian", "build_directory": "../builds", "headless": "false", From 0daac40e217a5ce6530c42b7b78015964706394c Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Tue, 13 Feb 2024 09:24:08 +0000 Subject: [PATCH 18/56] ci: amd64 base image update to debian 12.5.0 (#143) --- ci/amd64/debian/joininbox-amd64-debian.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index 718117e..32cf683 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -1,7 +1,7 @@ { "variables": { - "iso_name": "debian-12.4.0-amd64-netinst.iso", - "iso_checksum": "64d727dd5785ae5fcfd3ae8ffbede5f40cca96f1580aaa2820e8b99dae989d94", + "iso_name": "debian-12.5.0-amd64-netinst.iso", + "iso_checksum": "013f5b44670d81280b5b1bc02455842b250df2f0c6763398feb69af1a805a14f", "box_basename": "debian", "build_directory": "../builds", "headless": "false", From a61da715ca4106f922d77b1b3357bcc49c73d7d9 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Wed, 14 Feb 2024 07:44:38 +0000 Subject: [PATCH 19/56] build: fix github web-flow PGPpubkeyFingerprint (#144) --- build_joininbox.sh | 4 ++-- scripts/verify.git.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build_joininbox.sh b/build_joininbox.sh index 3029fb0..54d9421 100644 --- a/build_joininbox.sh +++ b/build_joininbox.sh @@ -400,11 +400,11 @@ if echo "${lastCommit}" | grep 13C688DB5B9C745DE4D2E4545BFB77609B081B65; then PGPsigner="openoms" PGPpubkeyLink="https://github.com/openoms.gpg" PGPpubkeyFingerprint="13C688DB5B9C745DE4D2E4545BFB77609B081B65" -elif echo "${lastCommit}" | grep 4AEE18F83AFDEB23; then +elif echo "${lastCommit}" | grep B5690EEEBB952194; then echo "# The last commit was made on GitHub and is signed with the GitHub PGP key." PGPsigner="web-flow" PGPpubkeyLink="https://github.com/${PGPsigner}.gpg" - PGPpubkeyFingerprint="4AEE18F83AFDEB23" + PGPpubkeyFingerprint="B5690EEEBB952194" else echo "# No known PGP key found" exit 1 diff --git a/scripts/verify.git.sh b/scripts/verify.git.sh index a687c04..89ded16 100644 --- a/scripts/verify.git.sh +++ b/scripts/verify.git.sh @@ -15,7 +15,7 @@ fi # Example for commits created on GitHub: # PGPsigner="web-flow" # PGPpubkeyLink="https://github.com/${PGPsigner}.gpg" -# PGPpubkeyFingerprint="4AEE18F83AFDEB23" +# PGPpubkeyFingerprint="B5690EEEBB952194" # Example for commits signed with a personal PGP key: # PGPsigner="janoside" From ea5187c93309687232712a9b2c79745015071b1d Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 26 Feb 2024 11:06:00 +0000 Subject: [PATCH 20/56] joinmarket update to v0.9.11 (#145) * joinmarket update to v0.9.11 * remove deprecated install tweak * remove pyhton 3.7 support * stop using system-side PySide2 * don't install the QT Gui on ARM * format start.joininbox.sh * remove arm specific qtgui deps, no qtgui on build for arm * don't protect system packages from pip install * add libltdl-dev to apt-get packages --- build_joininbox.sh | 98 +++++++++++----------------- scripts/_functions.sh | 1 - scripts/install.joinmarket.sh | 37 +---------- scripts/start.joininbox.sh | 119 +++++++++++++++++----------------- 4 files changed, 99 insertions(+), 156 deletions(-) diff --git a/build_joininbox.sh b/build_joininbox.sh index 54d9421..9883eba 100644 --- a/build_joininbox.sh +++ b/build_joininbox.sh @@ -266,69 +266,38 @@ echo "##########" echo # apt dependencies for python apt-get install -y python3 virtualenv python3-venv python3-dev python3-wheel python3-jinja2 python3-pip -if [ "${cpu}" = "armv7l" ] || [ "${cpu}" = "armv6l" ]; then - if [ ! -f "/usr/bin/python3.7" ]; then - # install python37 - pythonVersion="3.7.9" - majorPythonVersion=$(echo "$pythonVersion" | awk -F. '{print $1"."$2}') - # dependencies - apt-get install software-properties-common build-essential libnss3-dev zlib1g-dev libgdbm-dev libncurses5-dev libssl-dev libffi-dev libreadline-dev libsqlite3-dev libbz2-dev -y - # download - wget --prefer-family=ipv4 --progress=bar:force https://www.python.org/ftp/python/${pythonVersion}/Python-${pythonVersion}.tgz - # optional signature for verification - wget --prefer-family=ipv4 --progress=bar:force https://www.python.org/ftp/python/${pythonVersion}/Python-${pythonVersion}.tgz.asc - # get PGP pubkey of Ned Deily (Python release signing key) - gpg --recv-key 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D - # check for: Good signature from "Pablo Galindo Salgado " - gpg --verify Python-${pythonVersion}.tgz.asc || exit 1 - # unzip - tar xvf Python-${pythonVersion}.tgz - cd Python-${pythonVersion} || exit 1 - # configure - ./configure --enable-optimizations - # install - make altinstall - # move the python binary to the expected directory - mv "$(which python${majorPythonVersion})" /usr/bin/ - # check - ls -la /usr/bin/python${majorPythonVersion} || exit 1 - # clean - cd .. - rm Python-${pythonVersion}.tgz - rm -rf Python-${pythonVersion} - fi - update-alternatives --install /usr/bin/python python /usr/bin/python3.7 1 - echo "# python calls python3.7" - +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" +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" else - if [ -f "/usr/bin/python3.7" ]; then - # make sure /usr/bin/python exists (and calls Python3.7) - update-alternatives --install /usr/bin/python python /usr/bin/python3.7 1 - echo "# python calls python3.7" - 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" - 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" - 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" - else - echo "# FAIL- there is no tested version of python present" - exit 1 - fi + echo "# FAIL- there is no tested version of python present" + exit 1 fi # make sure /usr/bin/pip exists (and calls pip3) update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1 + +# don't protect system packages from pip install +# tracking issue: https://github.com/raspiblitz/raspiblitz/issues/4170 +for PYTHONDIR in /usr/lib/python3.*; do + if [ -f "$PYTHONDIR/EXTERNALLY-MANAGED" ]; then + rm "$PYTHONDIR/EXTERNALLY-MANAGED" + fi +done + # setuptools needed for Nyx pip install setuptools @@ -359,6 +328,8 @@ apt-get install -y dialog apt-get install -y qrencode # unzip for the pruned node snapshot apt-get install -y unzip +# JoinMarket dependency https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/5bfa08c6f558458c9a93e8095ce9dc1b62412838/install.sh#L76C10-L76C21 +apt-get install -y libltdl-dev apt-get clean apt-get -y autoremove @@ -593,7 +564,6 @@ echo "#############" echo " if [ -f \"/home/joinmarket/joinmarket-clientserver/jmvenv/bin/activate\" ]; then . /home/joinmarket/joinmarket-clientserver/jmvenv/bin/activate - /home/joinmarket/joinmarket-clientserver/jmvenv/bin/python -c \"import PySide2\" cd /home/joinmarket/joinmarket-clientserver/scripts/ fi # shortcut commands @@ -616,10 +586,16 @@ echo "######################" echo "# Install JoinMarket" echo "######################" -qtgui=true + +if [ "${cpu}" = x86_64 ]; then + qtgui=true +else + # no qtgui on arm + qtgui=false +fi checkEntry=$(runuser joinmarket -c "cat /home/joinmarket/joinin.conf | grep -c qtgui") if [ ${checkEntry} -eq 0 ]; then - echo "qtgui=true" | tee -a /home/joinmarket/joinin.conf + echo "qtgui=$qtgui" | tee -a /home/joinmarket/joinin.conf fi if [ "$4" = "without-qt" ]; then qtgui="false" diff --git a/scripts/_functions.sh b/scripts/_functions.sh index fc68e64..781ea2f 100755 --- a/scripts/_functions.sh +++ b/scripts/_functions.sh @@ -23,7 +23,6 @@ fi function activateJMvenv() { . /home/joinmarket/joinmarket-clientserver/jmvenv/bin/activate || exit 1 - /home/joinmarket/joinmarket-clientserver/jmvenv/bin/python -c "import PySide2" } function openMenuIfCancelled() { diff --git a/scripts/install.joinmarket.sh b/scripts/install.joinmarket.sh index 2251308..4a2248d 100755 --- a/scripts/install.joinmarket.sh +++ b/scripts/install.joinmarket.sh @@ -1,6 +1,7 @@ #!/bin/bash -testedJMversion="v0.9.10" +# https://github.com/JoinMarket-Org/joinmarket-clientserver/releases +testedJMversion="v0.9.11" PGPsigner="kristapsk" PGPpkeys="https://github.com/kristapsk.gpg" @@ -140,7 +141,6 @@ if [ "${user}" != "joinmarket" ]; then echo " if [ -f \"/home/${user}/joinmarket-clientserver/jmvenv/bin/activate\" ]; then . /home/${user}/joinmarket-clientserver/jmvenv/bin/activate - /home/${user}/joinmarket-clientserver/jmvenv/bin/python -c \"import PySide2\" cd /home/${user}/joinmarket-clientserver/scripts/ fi " | sudo -u ${user} tee -a /home/${user}/.bashrc @@ -158,14 +158,7 @@ fi # installJoinMarket [update|testPR |commit] function installJoinMarket() { - cpu=$(uname -m) cd /home/${user} || exit 1 - if [ "${qtgui}" = "true" ]; then - # PySide2 for armf: https://packages.debian.org/buster/python3-pyside2.qtcore - echo "# Installing ARM specific dependencies to run the QT GUI" - sudo apt-get install -y python3-pyside2.qtcore python3-pyside2.qtgui \ - python3-pyside2.qtwidgets zlib1g-dev libjpeg-dev python3-pyqt5 libltdl-dev - fi # https://github.com/JoinMarket-Org/joinmarket-clientserver/issues/668#issuecomment-717815719 sudo apt-get install -y build-essential automake pkg-config libffi-dev python3-dev sudo -u ${user} pip install libtool asn1crypto cffi pycparser @@ -202,7 +195,7 @@ function installJoinMarket() { 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 key + read -r fi sudo -u ${user} gpg --import ./pgp_keys.asc @@ -232,30 +225,6 @@ function installJoinMarket() { fi # do not clear screen during installation sudo -u ${user} sed -i 's/clear//g' install.sh - # do not stop at installing Debian dependencies - sudo -u ${user} sed -i \ - "s#^ if ! sudo apt-get install \${deb_deps\[@\]}; then#\ - if ! sudo apt-get install -y \${deb_deps\[@\]}; then#g" install.sh - - if [ ${cpu} != "x86_64" ]; then - echo "# Make install.sh set up jmvenv with -- system-site-packages on arm" - # and import the PySide2 armf package from the system - sudo -u ${user} sed -i "s#^ virtualenv -p \"\${python}\" \"\${jm_source}/jmvenv\" || return 1#\ - virtualenv --system-site-packages -p \"\${python}\" \"\${jm_source}/jmvenv\" || return 1 ;\ - /home/${user}/joinmarket-clientserver/jmvenv/bin/python -c \'import PySide2\'\ - #g" install.sh - # don't install PySide2 - using the system-site-package instead - sudo -u ${user} sed -i "s#^PySide2.*##g" requirements/gui.txt - # don't install PyQt5 - using the system package instead - sudo -u ${user} sed -i "s#^PyQt5.*##g" requirements/gui.txt - sudo -u ${user} sed -i "s#PyQt5!=5.15.0,!=5.15.1,!=5.15.2,!=6.0##g" jmqtui/setup.py - fi - - # pin werkzeug dependency to 2.2.0 as in: - # https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/1485/files - if ! grep 'werkzeug==' jmclient/setup.py; then - sed -i "s/autobahn==20.12.3/&', 'werkzeug==2.2.0/g" jmclient/setup.py - fi if [ "${qtgui}" = "false" ]; then GUIchoice="--without-qt" diff --git a/scripts/start.joininbox.sh b/scripts/start.joininbox.sh index d95fdcb..fd8dfa6 100755 --- a/scripts/start.joininbox.sh +++ b/scripts/start.joininbox.sh @@ -2,7 +2,7 @@ echo "# starting JoininBox ..." -if [ ! -f /home/joinmarket/joinin.conf ];then +if [ ! -f /home/joinmarket/joinin.conf ]; then touch /home/joinmarket/joinin.conf fi @@ -12,45 +12,44 @@ source /home/joinmarket/_functions.sh # FIRST RUN # ############# -setupStepEntry=$(grep -c "setupStep" < $joininConfPath) -if [ "$setupStepEntry" -eq 0 ];then - echo "setupStep=0" >> $joininConfPath +setupStepEntry=$(grep -c "setupStep" <$joininConfPath) +if [ "$setupStepEntry" -eq 0 ]; then + echo "setupStep=0" >>$joininConfPath fi source /home/joinmarket/joinin.conf -if [ "$setupStep" -lt 100 ];then - if [ "$setupStep" -lt 5 ];then +if [ "$setupStep" -lt 100 ]; then + if [ "$setupStep" -lt 5 ]; then # identify running env - runningEnvEntry=$(grep -c "runningEnv" < $joininConfPath) - if [ "$runningEnvEntry" -eq 0 ];then - if [ -f "/mnt/hdd/raspiblitz.conf" ];then + runningEnvEntry=$(grep -c "runningEnv" <$joininConfPath) + if [ "$runningEnvEntry" -eq 0 ]; then + if [ -f "/mnt/hdd/raspiblitz.conf" ]; then runningEnv="raspiblitz" - elif [ -f "/usr/share/mynode/mynode_config.sh" ];then + elif [ -f "/usr/share/mynode/mynode_config.sh" ]; then runningEnv="mynode" else runningEnv="standalone" fi - echo "runningEnv=$runningEnv" >> $joininConfPath - sed -i "s#setupStep=.*#setupStep=1#g" $joininConfPath + echo "runningEnv=$runningEnv" >>$joininConfPath + sed -i "s#setupStep=.*#setupStep=1#g" $joininConfPath fi echo "# running in the environment: $runningEnv" # identify cpu architecture - cpuEntry=$(grep -c "cpu" < $joininConfPath) - if [ "$cpuEntry" -eq 0 ];then + cpuEntry=$(grep -c "cpu" <$joininConfPath) + if [ "$cpuEntry" -eq 0 ]; then cpu=$(uname -m) - echo "cpu=$cpu" >> $joininConfPath - sed -i "s#setupStep=.*#setupStep=2#g" $joininConfPath + echo "cpu=$cpu" >>$joininConfPath + sed -i "s#setupStep=.*#setupStep=2#g" $joininConfPath fi echo "# cpu=${cpu}" # check Tor - torEntry=$(grep -c "runBehindTor" < $joininConfPath) - if [ "$torEntry" -eq 0 ];then + torEntry=$(grep -c "runBehindTor" <$joininConfPath) + if [ "$torEntry" -eq 0 ]; then torTest=$(curl --socks5 localhost:9050 --socks5-hostname localhost:9050 -s \ - https://check.torproject.org/ | cat | grep -m 1 Congratulations | xargs) - if [ "$torTest" = "Congratulations. This browser is configured to use Tor." ] - then + https://check.torproject.org/ | cat | grep -m 1 Congratulations | xargs) + if [ "$torTest" = "Congratulations. This browser is configured to use Tor." ]; then runBehindTor=on else runBehindTor=off @@ -59,65 +58,65 @@ if [ "$setupStep" -lt 100 ];then echo "# Press ENTER to continue without Tor or CTRL+C to cancel and try checking again with 'menu'" read key fi - echo "runBehindTor=$runBehindTor" >> $joininConfPath + echo "runBehindTor=$runBehindTor" >>$joininConfPath echo "# runBehindTor=$runBehindTor" fi # make sure Tor path is known - DirEntry=$(grep -c "HiddenServiceDir" < $joininConfPath) - if [ "$DirEntry" -eq 0 ];then - if [ -d "/mnt/hdd/tor" ];then + DirEntry=$(grep -c "HiddenServiceDir" <$joininConfPath) + if [ "$DirEntry" -eq 0 ]; then + if [ -d "/mnt/hdd/tor" ]; then HiddenServiceDir="/mnt/hdd/tor" else HiddenServiceDir="/var/lib/tor" fi - echo "HiddenServiceDir=$HiddenServiceDir" >> $joininConfPath - sed -i "s#setupStep=.*#setupStep=3#g" $joininConfPath + echo "HiddenServiceDir=$HiddenServiceDir" >>$joininConfPath + sed -i "s#setupStep=.*#setupStep=3#g" $joininConfPath fi # check for dialog - if [ "$(dialog | grep -c "ComeOn Dialog!")" -eq 0 ];then + if [ "$(dialog | grep -c "ComeOn Dialog!")" -eq 0 ]; then sudo apt-get install -y dialog fi # check for qrencode - if [ "$(qrencode -V 2>&1 | grep -c "not found")" -gt 0 ];then + if [ "$(qrencode -V 2>&1 | grep -c "not found")" -gt 0 ]; then sudo apt-get install -y qrencode fi - sed -i "s#setupStep=.*#setupStep=4#g" $joininConfPath + sed -i "s#setupStep=.*#setupStep=4#g" $joininConfPath # check if JoinMarket is installed - if [ "${runningEnv}" = raspiblitz ] && [ "$(lsb_release -sc)" = bullseye ]; then - # no qtgui for raspibliz on bullseye - /home/joinmarket/install.joinmarket.sh -i install -q 0 - else + if [ "${cpu}" = x86_64 ]; then /home/joinmarket/install.joinmarket.sh -i install + else + # no qtgui on arm + /home/joinmarket/install.joinmarket.sh -i install -q 0 fi - sed -i "s#setupStep=.*#setupStep=5#g" $joininConfPath + sed -i "s#setupStep=.*#setupStep=5#g" $joininConfPath fi # change the ssh password if standalone - if [ "$runningEnv" = "standalone" ];then + if [ "$runningEnv" = "standalone" ]; then source /home/joinmarket/joinin.conf - if [ "$setupStep" -lt 6 ];then + if [ "$setupStep" -lt 6 ]; then # set ssh passwords on the first run sudo /home/joinmarket/set.password.sh - sed -i "s#setupStep=.*#setupStep=6#g" $joininConfPath + sed -i "s#setupStep=.*#setupStep=6#g" $joininConfPath fi source /home/joinmarket/joinin.conf - if [ "$setupStep" -lt 7 ]&&[ ${cpu} != "x86_64" ];then + if [ "$setupStep" -lt 7 ] && [ ${cpu} != "x86_64" ]; then # expand SDcard partition on ARM sudo /home/joinmarket/standalone/expand.rootfs.sh fi fi generateJMconfig - sudo sed -i "s#setupStep=.*#setupStep=10#g" $joininConfPath + sudo sed -i "s#setupStep=.*#setupStep=10#g" $joininConfPath source /home/joinmarket/joinin.conf - if [ "$setupStep" -lt 11 ];then - if [ "$runningEnv" = "standalone" ];then + if [ "$setupStep" -lt 11 ]; then + if [ "$runningEnv" = "standalone" ]; then # open the config menu if standalone /home/joinmarket/menu.config.sh else # setup finished - sudo sed -i "s#setupStep=.*#setupStep=100#g" $joininConfPath + sudo sed -i "s#setupStep=.*#setupStep=100#g" $joininConfPath fi fi fi @@ -128,11 +127,11 @@ fi # check bitcoind RPC setting # add default value to joinin config if needed -if ! grep -Eq "^RPCoverTor=" $joininConfPath;then - echo "RPCoverTor=off" >> $joininConfPath +if ! grep -Eq "^RPCoverTor=" $joininConfPath; then + echo "RPCoverTor=off" >>$joininConfPath fi # check if bitcoin RPC connection is over Tor -if grep -Eq "^rpc_host = .*.onion" $JMcfgPath;then +if grep -Eq "^rpc_host = .*.onion" $JMcfgPath; then echo "# RPC over Tor is on" sed -i "s/^RPCoverTor=.*/RPCoverTor=on/g" $joininConfPath else @@ -142,14 +141,14 @@ fi # check if there is only one joinmarket wallet and make default # add default value to joinin config if needed -if ! grep -Eq "^defaultWallet=" $joininConfPath;then - echo "defaultWallet=off" >> $joininConfPath +if ! grep -Eq "^defaultWallet=" $joininConfPath; then + echo "defaultWallet=off" >>$joininConfPath fi -if [ "$(ls -p /home/joinmarket/.joinmarket/wallets/ | grep -cv /)" -gt 1 ];then +if [ "$(ls -p /home/joinmarket/.joinmarket/wallets/ | grep -cv /)" -gt 1 ]; then echo "# Found more than one wallet file" echo "# Setting defaultWallet to off" sed -i "s#^defaultWallet=.*#defaultWallet=off#g" $joininConfPath -elif [ "$(ls -p /home/joinmarket/.joinmarket/wallets/ | grep -cv /)" -eq 1 ];then +elif [ "$(ls -p /home/joinmarket/.joinmarket/wallets/ | grep -cv /)" -eq 1 ]; then onlyWallet=$(ls -p /home/joinmarket/.joinmarket/wallets/ | grep -v /) echo "# Found only one wallet file: $onlyWallet" echo "# Using it as default" @@ -157,25 +156,25 @@ elif [ "$(ls -p /home/joinmarket/.joinmarket/wallets/ | grep -cv /)" -eq 1 ];the fi # add default value to joinin config if needed -if ! grep -Eq "^network=" $joininConfPath;then - echo "network=unknown" >> $joininConfPath +if ! grep -Eq "^network=" $joininConfPath; then + echo "network=unknown" >>$joininConfPath fi -isMainnet=$(grep -c "network = mainnet" < $JMcfgPath) -isSignet=$(grep -c "network = signet" < $JMcfgPath) -isTestnet=$(grep -c "network = testnet" < $JMcfgPath) -if [ $isMainnet -gt 0 ];then +isMainnet=$(grep -c "network = mainnet" <$JMcfgPath) +isSignet=$(grep -c "network = signet" <$JMcfgPath) +isTestnet=$(grep -c "network = testnet" <$JMcfgPath) +if [ $isMainnet -gt 0 ]; then sed -i "s#^network=.*#network=mainnet#g" $joininConfPath -elif [ $isSignet -gt 0 ];then +elif [ $isSignet -gt 0 ]; then sed -i "s#^network=.*#network=signet#g" $joininConfPath -elif [ $isTestnet -gt 0 ];then +elif [ $isTestnet -gt 0 ]; then sed -i "s#^network=.*#network=testnet#g" $joininConfPath else sed -i "s#^network=.*#network=unknown#g" $joininConfPath fi # add default value to joinin config if needed -if ! grep -Eq "^localip=" $joininConfPath;then - echo "localip=unknown" >> $joininConfPath +if ! grep -Eq "^localip=" $joininConfPath; then + echo "localip=unknown" >>$joininConfPath fi localip=$(hostname -I | awk '{print $1}') sed -i "s#^localip=.*#localip=$localip#g" $joininConfPath From dec26c77322568450e04fd1b1edf653e19fceeb9 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 26 Feb 2024 21:05:46 +0000 Subject: [PATCH 21/56] jam-remote update to 0.2.0 (#147) --- .gitignore | 1 + scripts/jam-remote/install.jam.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0329ccb..6d5fe6c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ scripts/wallets ci/arm64-rpi/.packer* ci/arm64-rpi/joininbox-arm64-rpi* ci/amd64/builds +scripts/jam-remote/download diff --git a/scripts/jam-remote/install.jam.sh b/scripts/jam-remote/install.jam.sh index 5fe7958..449d5c3 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.1.6" +WEBUI_VERSION="v0.2.0" REPO=joinmarket-webui/jam HOME_DIR=/home/${USERNAME} APP_DIR=webui From 11c2e0dacd4196a21a6affe9249a7929d656a39d Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 26 Feb 2024 21:08:31 +0000 Subject: [PATCH 22/56] change qtgui var to the correct option on install (#146) --- scripts/install.joinmarket.sh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/install.joinmarket.sh b/scripts/install.joinmarket.sh index 4a2248d..9ba1b90 100755 --- a/scripts/install.joinmarket.sh +++ b/scripts/install.joinmarket.sh @@ -27,7 +27,7 @@ Options: -h, --help this help info -i, --install [install|config|update|testPR|commit] install options, use 'commit' for the latest master -v, --version [version|number-of-PR] the version to install or PR to test (default: ${testedJMversion}) --q, --qtgui [0|1] install the QT GUI and dependencies (default: 1) +-q, --qtgui [0|1] install the QT GUI and dependencies (default: 0) -u, --user [user] the linux user to install with (default: joinmarket) Notes: @@ -113,7 +113,7 @@ 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}'" -: "${qtgui:=true}" +: "${qtgui:=false}" range_argument qtgui "0" "1" "false" "true" : "${user:=joinmarket}" @@ -152,9 +152,7 @@ checkEntry=$(sudo -u ${user} cat /home/${user}/joinin.conf | grep -c "qtgui") if [ ${checkEntry} -eq 0 ]; then echo "qtgui=true" | sudo -u ${user} tee -a /home/${user}/joinin.conf fi -if [ "${qtgui}" = "false" ]; then - sudo -u ${user} sed -i "s/^qtgui=.*/qtgui=false/g" /home/${user}/joinin.conf -fi +sudo -u ${user} sed -i "s/^qtgui=.*/qtgui=${qtgui}/g" /home/${user}/joinin.conf # installJoinMarket [update|testPR |commit] function installJoinMarket() { From 0126005b902add8b6cd5ddf8ed313907669e024a Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 10 Mar 2024 20:39:14 +0000 Subject: [PATCH 23/56] fixes for pruned node start (#149) * increase required space for pruned chain download * don't protect home as bitcoind binary is there * format UDPATE menu --- scripts/_functions.bitcoincore.sh | 2 -- scripts/menu.update.sh | 2 +- scripts/standalone/_functions.standalone.sh | 14 +++++++------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/scripts/_functions.bitcoincore.sh b/scripts/_functions.bitcoincore.sh index 3952d4d..4b27a91 100755 --- a/scripts/_functions.bitcoincore.sh +++ b/scripts/_functions.bitcoincore.sh @@ -185,8 +185,6 @@ StandardError=journal PrivateTmp=true # Mount /usr, /boot/ and /etc read-only for the process. ProtectSystem=full -# Deny access to /home, /root and /run/user -ProtectHome=true # Disallow the process and all of its children to gain # new privileges through execve(). NoNewPrivileges=true diff --git a/scripts/menu.update.sh b/scripts/menu.update.sh index 336aca3..e854304 100755 --- a/scripts/menu.update.sh +++ b/scripts/menu.update.sh @@ -4,7 +4,7 @@ source /home/joinmarket/_functions.sh # BASIC MENU INFO HEIGHT=15 -WIDTH=56 +WIDTH=57 CHOICE_HEIGHT=4 TITLE="Update options" MENU=" diff --git a/scripts/standalone/_functions.standalone.sh b/scripts/standalone/_functions.standalone.sh index d80d2eb..7985a60 100755 --- a/scripts/standalone/_functions.standalone.sh +++ b/scripts/standalone/_functions.standalone.sh @@ -49,13 +49,13 @@ function downloadSnapShot() { echo "# Check available diskspace" FREE=$(df -k --output=avail "$PWD" | tail -n1) # df -k not df -h - if [ $FREE -lt 12582912 ]; then # 12G = 12*1024*1024k + if [ $FREE -lt 21000000 ]; then echo "# The free space is only $FREE bytes!" - echo "# Would need ~12GB free space to download and extract the snapshot." + echo "# Would need ~21GB free space to download and extract the snapshot." echo "# Press ENTER to continue to download regardless or CTRL+C to exit." read key else - echo "# OK, more than 12GB is free!" + echo "# OK, more than 21GB is free!" fi sudo -u joinmarket mkdir /home/joinmarket/download 2>/dev/null cd /home/joinmarket/download || exit 1 @@ -107,13 +107,13 @@ function downloadSnapShot() { echo "# Extracting to /home/store/app-data/.bitcoin ..." FREE=$(df -k --output=avail "$PWD" | tail -n1) # df -k not df -h - if [ $FREE -lt 7340032 ]; then # 7G = 7*1024*1024k + if [ $FREE -lt 11000000 ]; then echo "# The free space is only $FREE bytes!" - echo "# Would need ~7GB free space to extract the snapshot." - echo "# Press ENTER to continue to download regardless or CTRL+C to exit." + echo "# Would need ~11GB free space to extract the snapshot." + echo "# Press ENTER to continue to extract it regardless or CTRL+C to exit." read key else - echo "# OK, more than 7GB is free!" + echo "# OK, more than 11GB is free!" fi addUserStore if [ ! -d /home/store/app-data/.bitcoin ]; then From d961839137f4ff6282fe91eb66264c77fc1e2c6a Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Fri, 5 Apr 2024 08:33:08 +0200 Subject: [PATCH 24/56] prevent sleep and "unable to resolve host debian" error (#151) * prevent sleep and "unable to resolve host debian" error * fomat script --- build_joininbox.sh | 23 ++++++++++++++++++++++- scripts/set.ssh.sh | 6 +++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/build_joininbox.sh b/build_joininbox.sh index 9883eba..85bc85b 100644 --- a/build_joininbox.sh +++ b/build_joininbox.sh @@ -90,7 +90,28 @@ echo "# Preparing the base image" echo "############################" echo -echo "# Prepare ${baseImage} " +echo "# Prevent sleep" # on all platforms https://wiki.debian.org/Suspend +systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target +mkdir /etc/systemd/sleep.conf.d +echo "[Sleep] +AllowSuspend=no +AllowHibernation=no +AllowSuspendThenHibernate=no +AllowHybridSleep=no" | tee /etc/systemd/sleep.conf.d/nosuspend.conf +mkdir /etc/systemd/logind.conf.d +echo "[Login] +HandleLidSwitch=ignore +HandleLidSwitchDocked=ignore" | tee /etc/systemd/logind.conf.d/nosuspend.conf + +# check if /etc/hosts already has debian entry +# prevent "unable to resolve host debian" error +isDebianInHosts=$(grep -c "debian" /etc/hosts) +if [ ${isDebianInHosts} -eq 0 ]; then + echo "# Adding debian to /etc/hosts" + echo "127.0.1.1 debian" | tee -a /etc/hosts > /dev/null + systemctl restart networking +fi + # special prepare on RPi if [ "${baseimage}" = "raspios" ] || [ "${baseimage}" = "debian_rpi64" ] || [ "${baseimage}" = "armbian" ]; then diff --git a/scripts/set.ssh.sh b/scripts/set.ssh.sh index 2aee01c..21edcdd 100755 --- a/scripts/set.ssh.sh +++ b/scripts/set.ssh.sh @@ -1,6 +1,6 @@ #!/bin/bash -if [ ${#1} -eq 0 ]||[ $1 = "-h" ]||[ $1 = "--help" ];then +if [ ${#1} -eq 0 ] || [ $1 = "-h" ] || [ $1 = "--help" ]; then echo "Enable or disable ssh access with the joinmarket user" echo "sudo set.ssh.sh [off|on]" echo @@ -18,7 +18,7 @@ if ! grep -Eq "^joinmarketSSH=" /home/joinmarket/joinin.conf; then fi echo -if [ "$1" = "off" ];then +if [ "$1" = "off" ]; then echo "# Disable ssh access with the joinmarket user" if ! grep -Eq "^DenyUsers joinmarket" /etc/ssh/sshd_config; then echo "DenyUsers joinmarket" | tee -a /etc/ssh/sshd_config @@ -35,4 +35,4 @@ elif [ "$1" = "on" ]; then else echo "# Invalid option $*" exit 1 -fi \ No newline at end of file +fi From e2e540ccbd643ca5c205d6eac38e9edfb3b42eb2 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:58:10 +0200 Subject: [PATCH 25/56] bitcoin core update to 26.1 (#152) --- 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 4b27a91..013da91 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.0" + bitcoinVersion="26.1" if bitcoin-cli --version | grep $bitcoinVersion >/dev/null; then echo "# Bitcoin Core $bitcoinVersion is already installed" From efad4a01ff339972420f2d4f8a6e205dee9d3522 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Wed, 22 May 2024 12:27:02 +0200 Subject: [PATCH 26/56] Standalone setup fixes (#153) * use bitcoin binary in /usr/local/bin/ * fix hostname and related warnings * fix ownership of possibly migrated files * build: fix fail2ban, remove boot drive tweak * FAQ: remove outdated entries --- FAQ.md | 63 +-------------------- build_joininbox.sh | 16 +----- ci/amd64/debian/scripts/networking.sh | 3 + ci/arm64-rpi/arm64-rpi.pkr.hcl | 3 +- scripts/standalone/_functions.standalone.sh | 34 +++++------ scripts/standalone/bitcoin.update.sh | 6 +- scripts/standalone/bootstrap.sh | 4 ++ 7 files changed, 33 insertions(+), 96 deletions(-) diff --git a/FAQ.md b/FAQ.md index 97ea6c7..26bcfc2 100644 --- a/FAQ.md +++ b/FAQ.md @@ -567,26 +567,7 @@ Alternatively to a pruned node there could be a larger >400 GB storage connected # drwx------ 34 bitcoin bitcoin 4096 Dec 7 23:39 specter # drwx------ 2 bitcoin bitcoin 4096 Mar 21 10:38 wallet.dat installMainnet - # Failed to stop bitcoind.service: Unit bitcoind.service not loaded. - # - # [Unit] - # Description=Bitcoin daemon on mainnet - # [Service] - # User=bitcoin - # Group=bitcoin - # Type=forking - # PIDFile=/home/bitcoin/bitcoin/bitcoind.pid - # ExecStart=/home/bitcoin/bitcoin/bitcoind -daemon -pid=/home/bitcoin/bitcoin/bitcoind.pid - # Restart=always - # TimeoutSec=120 - # RestartSec=30 - # StandardOutput=null - # StandardError=journal - # - # [Install] - # WantedBy=multi-user.target - # - # Created symlink /etc/systemd/system/multi-user.target.wants/bitcoind.service → /etc/systemd/system/bitcoind.service. + ... # # OK - the bitcoind.service is now enabled # # # Installed Bitcoin Core version v0.21.0 @@ -604,48 +585,6 @@ Alternatively to a pruned node there could be a larger >400 GB storage connected ## IRC server settings * See the most up to date configuration in: https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/jmclient/jmclient/configure.py -* The latest setting with Tor enabled: - ``` - [MESSAGING:server1] - #host = irc.darkscience.net - channel = joinmarket-pit - port = 6697 - usessl = true - #socks5 = false - socks5_host = localhost - socks5_port = 9050 - #for tor - host = darkirc6tqgpnwd3blln3yfv5ckl47eg7llfxkmtovrv7c7iwohhb6ad.onion - socks5 = true - - [MESSAGING:server2] - #host = irc.hackint.org - channel = joinmarket-pit - #port = 6697 - #usessl = true - #socks5 = false - socks5_host = localhost - socks5_port = 9050 - #for tor - host = ncwkrwxpq2ikcngxq3dy2xctuheniggtqeibvgofixpzvrwpa77tozqd.onion - port = 6667 - usessl = false - socks5 = true - - [MESSAGING:server3] - #host = agora.anarplex.net - channel = joinmarket-pit - #port = 14716 - #usessl = true - #socks5 = false - socks5_host = localhost - socks5_port = 9050 - #for tor - host = vxecvd6lc4giwtasjhgbrr3eop6pzq6i5rveracktioneunalgqlwfad.onion - port = 6667 - usessl = false - socks5 = true - ``` ## Install JoinMarket without the QT GUI and dependencies * Run the build script with the options `BRANCH` `GITHUBUSER` `without-qt`: diff --git a/build_joininbox.sh b/build_joininbox.sh index 85bc85b..e8074c5 100644 --- a/build_joininbox.sh +++ b/build_joininbox.sh @@ -52,7 +52,7 @@ echo " # https://github.com/${githubUser}/joininbox/tree/${wantedBranch} # Press ENTER to confirm or CTRL+C to exit" -read key +read echo echo "###################################" @@ -160,17 +160,6 @@ if [ "${baseimage}" = "raspios" ] || [ "${baseimage}" = "debian_rpi64" ]; then echo "$max_usb_current already in $configFile" fi - # run fsck on sd root partition on every startup to prevent "maintenance login" screen - # see: https://github.com/rootzoll/raspiblitz/issues/782#issuecomment-564981630 - # see https://github.com/rootzoll/raspiblitz/issues/1053#issuecomment-600878695 - # use command to check last fsck check: sudo tune2fs -l /dev/mmcblk0p2 - if [ "${tweak_boot_drive}" == "true" ]; then - echo "* running tune2fs" - tune2fs -c 1 /dev/mmcblk0p2 - else - echo "* skipping tweak_boot_drive" - fi - # edit kernel parameters kernelOptionsFile=/boot/cmdline.txt fsOption1="fsck.mode=force" @@ -485,7 +474,7 @@ deb-src [arch=${arch}] https://deb.torproject.org/torproject.org ${distro} main" apt-get install -y build-essential fakeroot devscripts apt-get build-dep -y tor deb.torproject.org-keyring mkdir ~/debian-packages - cd ~/debian-packages + cd ~/debian-packages || exit 1 apt-get source tor cd tor-* || exit 1 debuild -rfakeroot -uc -us @@ -533,6 +522,7 @@ echo apt-get install -y fail2ban ufw # autostart fail2ban systemctl enable fail2ban +touch /var/log/auth.log # set up the firewall ufw default deny incoming diff --git a/ci/amd64/debian/scripts/networking.sh b/ci/amd64/debian/scripts/networking.sh index a574c69..ec32d77 100644 --- a/ci/amd64/debian/scripts/networking.sh +++ b/ci/amd64/debian/scripts/networking.sh @@ -7,3 +7,6 @@ update-grub; # Adding a 2 sec delay to the interface up, to make the dhclient happy echo "pre-up sleep 2" >> /etc/network/interfaces + +echo "$(hostname -I | awk '{print $1}') $(hostname)" >>/etc/hosts +echo "127.0.1.1 $(hostname)" >>/etc/hosts diff --git a/ci/arm64-rpi/arm64-rpi.pkr.hcl b/ci/arm64-rpi/arm64-rpi.pkr.hcl index 7592b1b..fe27d74 100644 --- a/ci/arm64-rpi/arm64-rpi.pkr.hcl +++ b/ci/arm64-rpi/arm64-rpi.pkr.hcl @@ -39,7 +39,8 @@ build { inline = [ "echo 'nameserver 1.1.1.1' >/etc/resolv.conf", "echo 'nameserver 8.8.8.8' >>/etc/resolv.conf", - "echo $(hostname -I | awk '{print $1}') $(hostname) >>/etc/hosts", + "echo $(hostname -I | awk '{print $1}') $(hostname) >>/etc/hosts", + "echo 127.0.1.1 $(hostname) >>/etc/hosts", "echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections", "apt-get update", "apt-get install -y sudo wget", diff --git a/scripts/standalone/_functions.standalone.sh b/scripts/standalone/_functions.standalone.sh index 7985a60..207486f 100755 --- a/scripts/standalone/_functions.standalone.sh +++ b/scripts/standalone/_functions.standalone.sh @@ -140,7 +140,11 @@ function downloadSnapShot() { echo "# Unzip ..." sudo apt-get install -y unzip - sudo -u bitcoin unzip -o $downloadFileName -d /home/store/app-data/.bitcoin + sudo -u bitcoin unzip -o $downloadFileName -d /home/store/app-data/.bitcoin || exit 1 + echo "# OK - Decompressed successfully" + echo "# Removing the downloaded files" + rm -f $hashFileName + rm -f $downloadFileName if sudo -u bitcoin ls /home/bitcoin/.bitcoin/bitcoin.conf.backup; then echo "# Restore bitcoin.conf" sudo -u bitcoin mv -f /home/bitcoin/.bitcoin/bitcoin.conf.backup \ @@ -151,8 +155,8 @@ function downloadSnapShot() { function installBitcoinCoreStandalone() { downloadBitcoinCore - if [ -f /home/bitcoin/bitcoin/bitcoind ]; then - installedVersion=$(/home/bitcoin/bitcoin/bitcoind --version | grep version) + if [ -f /usr/local/bin/bitcoind ]; then + installedVersion=$(/usr/local/bin/bitcoind --version | grep version) echo "${installedVersion} is already installed" else echo "# Adding the user: bitcoin" @@ -162,15 +166,10 @@ function installBitcoinCoreStandalone() { echo "# Add the joinmarket user to the bitcoin group" sudo usermod -aG bitcoin joinmarket echo "# Installing Bitcoin Core v${bitcoinVersion}" - sudo -u bitcoin mkdir -p /home/bitcoin/bitcoin cd /home/joinmarket/download/bitcoin-${bitcoinVersion}/bin/ || exit 1 - sudo install -m 0755 -o root -g root -t /home/bitcoin/bitcoin ./* + sudo install -m 0755 -o root -g root -t /usr/local/bin/ ./* fi - if [ "$(grep -c "/home/bitcoin/bitcoin" Was not able to install Bitcoin Core)" exit 1 @@ -223,9 +222,10 @@ Description=Bitcoin daemon on mainnet [Service] Environment='MALLOC_ARENA_MAX=1' -PIDFile=/home/bitcoin/bitcoin/bitcoind.pid -ExecStart=/home/bitcoin/bitcoin/bitcoind -daemon \\ - -pid=/home/bitcoin/bitcoin/bitcoind.pid +ExecStart=/usr/local/bin/bitcoind \\ + -daemonwait \\ + -conf=/home/bitcoin/.bitcoin/bitcoin.conf \\ + -datadir=/home/bitcoin/.bitcoin PermissionsStartOnly=true # Process management @@ -270,10 +270,10 @@ WantedBy=multi-user.target # add aliases if ! grep "alias bitcoin-cli" /home/joinmarket/_aliases.sh; then - sudo bash -c "echo 'alias bitcoin-cli=\"sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli\"' >> /home/joinmarket/_aliases.sh" + sudo bash -c "echo 'alias bitcoin-cli=\"sudo -u bitcoin /usr/local/bin/bitcoin-cli\"' >> /home/joinmarket/_aliases.sh" fi if ! grep "alias bitcoind" /home/joinmarket/_aliases.sh; then - sudo bash -c "echo 'alias bitcoind=\"sudo -u bitcoin /home/bitcoin/bitcoin/bitcoind\"' >> /home/joinmarket/_aliases.sh" + sudo bash -c "echo 'alias bitcoind=\"sudo -u bitcoin /usr/local/bin/bitcoind\"' >> /home/joinmarket/_aliases.sh" fi if ! grep "alias bitcoinlog" /home/joinmarket/_aliases.sh; then sudo bash -c "echo 'alias bitcoinlog=\"sudo tail -f /home/bitcoin/.bitcoin/debug.log\"' >> /home/joinmarket/_aliases.sh" @@ -287,7 +287,7 @@ WantedBy=multi-user.target sudo systemctl start bitcoind echo - echo "# Installed $(sudo -u bitcoin /home/bitcoin/bitcoin/bitcoind --version | grep version)" + echo "# Installed $(sudo -u bitcoin /usr/local/bin/bitcoind --version | grep version)" echo echo "# Monitor the bitcoind with: sudo tail -f /home/bitcoin/.bitcoin/mainnet/debug.log" echo @@ -295,6 +295,6 @@ WantedBy=multi-user.target if [ ! -f /home/bitcoin/.bitcoin/mainnet/wallets/wallet.dat/wallet.dat ]; then echo "# Create wallet.dat ..." sleep 10 - sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli -named createwallet wallet_name=wallet.dat descriptors=false + sudo -u bitcoin /usr/local/bin/bitcoin-cli -named createwallet wallet_name=wallet.dat descriptors=false fi } diff --git a/scripts/standalone/bitcoin.update.sh b/scripts/standalone/bitcoin.update.sh index d5ffe4d..2e9a9ce 100644 --- a/scripts/standalone/bitcoin.update.sh +++ b/scripts/standalone/bitcoin.update.sh @@ -37,7 +37,7 @@ elif [ "$(uname -m | grep -c 'x86_64')" -gt 0 ]; then fi # installed version -installedVersion=$(sudo -u bitcoin /home/bitcoin/bitcoin/bitcoind --version | head -n1 | cut -d" " -f4 | cut -c 2-) +installedVersion=$(sudo -u bitcoin /usr/local/bin/bitcoind --version | head -n1 | cut -d" " -f4 | cut -c 2-) # test if the installed version already the tested/recommended update version bitcoinUpdateInstalled=$(echo "${installedVersion}" | grep -c "${bitcoinVersion}") @@ -202,9 +202,9 @@ if [ "${mode}" = "tested" ] || [ "${mode}" = "reckless" ] || [ "${mode}" = "cust echo echo "# Installing Bitcoin Core v${bitcoinVersion}" tar -xvf ${binaryName} - sudo install -m 0755 -o root -g root -t /home/bitcoin/bitcoin/ bitcoin-${bitcoinVersion}/bin/* + sudo install -m 0755 -o root -g root -t /usr/local/bin/ bitcoin-${bitcoinVersion}/bin/* sleep 3 - if ! sudo /home/bitcoin/bitcoin/bitcoind --version | grep "${bitcoinVersion}"; then + if ! sudo /usr/local/bin/bitcoind --version | grep "${bitcoinVersion}"; then echo echo "# BUILD FAILED --> Was not able to install bitcoind version(${bitcoinVersion})" exit 1 diff --git a/scripts/standalone/bootstrap.sh b/scripts/standalone/bootstrap.sh index 305d58d..8bf6c4c 100755 --- a/scripts/standalone/bootstrap.sh +++ b/scripts/standalone/bootstrap.sh @@ -21,3 +21,7 @@ echo "***********************************************" >> $logFile # make sure SSH server is configured & running sudo /home/joinmarket/standalone/ssh.sh checkrepair >> ${logFile} + +# fix ownership of possibly migrated files +sudo chown -R bitcoin:bitcoin /home/bitcoin/ +sudo chown -R joinmarket:joinmarket /home/joinmarket/ From c87e06c829955224f545dd6e938a37c2a91babc5 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Wed, 3 Jul 2024 10:03:59 +0200 Subject: [PATCH 27/56] update amd64 base image to debian-12.6.0-amd64-netinst.iso (#155) * update amd64 base image to debian-12.6.0-amd64-netinst.iso * fix apt sources for amd64 image --- ci/amd64/debian/http/debian-9/preseed.cfg | 2 +- ci/amd64/debian/joininbox-amd64-debian.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/amd64/debian/http/debian-9/preseed.cfg b/ci/amd64/debian/http/debian-9/preseed.cfg index a190f0b..4bc9d0d 100644 --- a/ci/amd64/debian/http/debian-9/preseed.cfg +++ b/ci/amd64/debian/http/debian-9/preseed.cfg @@ -9,7 +9,7 @@ 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 httpredir.debian.org +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 diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index 32cf683..acaafb3 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -1,7 +1,7 @@ { "variables": { - "iso_name": "debian-12.5.0-amd64-netinst.iso", - "iso_checksum": "013f5b44670d81280b5b1bc02455842b250df2f0c6763398feb69af1a805a14f", + "iso_name": "debian-12.6.0-amd64-netinst.iso", + "iso_checksum": "ade3a4acc465f59ca2496344aab72455945f3277a52afc5a2cae88cdc370fa12", "box_basename": "debian", "build_directory": "../builds", "headless": "false", From 622847e80e7ea0ee806db9bc7a181502e964177f Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 14 Jul 2024 14:16:15 +0200 Subject: [PATCH 28/56] feat: add i2pd install script (#156) * feat: add i2pd install script * add comments to addseednodes * limit addseednodes to 21 new random peers --- scripts/standalone/install.i2pd.sh | 296 +++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 scripts/standalone/install.i2pd.sh diff --git a/scripts/standalone/install.i2pd.sh b/scripts/standalone/install.i2pd.sh new file mode 100644 index 0000000..2c8561d --- /dev/null +++ b/scripts/standalone/install.i2pd.sh @@ -0,0 +1,296 @@ +#!/bin/bash + +# https://i2pd.readthedocs.io + +if [ $# -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "-help" ]; then + echo "I2P Daemon install script" + echo "More info at https://i2pd.readthedocs.io" + echo "Usage:" + echo "install.i2pd.sh install -> Install i2pd" + echo "install.i2pd.sh on -> Switch on i2pd" + echo "install.i2pd.sh off -> Uninstall i2pd" + echo "install.i2pd.sh addseednodes -> Add 21 randonly selected I2P seed nodes from: https://github.com/bitcoin/bitcoin/blob/master/contrib/seeds/nodes_main.txt" + echo "install.i2pd.sh status -> I2P related logs from bitcoind, bitcoin-cli -netinfo 4 and webconsole access" + exit 1 +fi + +function confAdd { + # get parameters + keystr="$1" + valuestr=$(echo "$2" | sed 's/\//\\\//g') + configFile="$3" + + # check if key needs to be added (prepare new entry) + entryExists=$(grep -c "^${keystr}=" ${configFile}) + if [ ${entryExists} -eq 0 ]; then + echo "${keystr}=" | sudo tee -a ${configFile} 1>/dev/null + fi + + # add an extra key=value line (needs sudo to operate when user is not root) + echo "${keystr}=${valuestr}" | sudo tee -a ${configFile} +} + +function add_repo { + # Add repo for the latest version + # i2pd — https://repo.i2pd.xyz/.help/readme.txt + # https://repo.i2pd.xyz/.help/add_repo + + source /etc/os-release + DIST=$ID + case $ID in + debian | ubuntu | raspbian) + if [[ -n $DEBIAN_CODENAME ]]; then + VERSION_CODENAME=$DEBIAN_CODENAME + fi + if [[ -n $UBUNTU_CODENAME ]]; then + VERSION_CODENAME=$UBUNTU_CODENAME + fi + if [[ -z $VERSION_CODENAME ]]; then + echo "Couldn't find VERSION_CODENAME in your /etc/os-release file. Did your system supported? Please report issue to me by writing to email: 'r4sas i2pd.xyz'" + exit 1 + fi + RELEASE=$VERSION_CODENAME + ;; + *) + if [[ -z $ID_LIKE || "$ID_LIKE" != "debian" && "$ID_LIKE" != "ubuntu" ]]; then + echo "Your system is not supported by this script. Currently it supports debian-like and ubuntu-like systems." + exit 1 + else + DIST=$ID_LIKE + case $ID_LIKE in + debian) + if [[ "$ID" == "kali" ]]; then + if [[ "$VERSION" == "2019"* || "$VERSION" == "2020"* ]]; then + RELEASE="buster" + elif [[ "$VERSION" == "2021"* || "$VERSION" == "2022"* ]]; then + RELEASE="bullseye" + fi + else + RELEASE=$DEBIAN_CODENAME + fi + ;; + ubuntu) + RELEASE=$UBUNTU_CODENAME + ;; + esac + fi + ;; + esac + if [[ -z $RELEASE ]]; then + echo "Couldn't detect your system release. Please report issue to me by writing to email: 'r4sas i2pd.xyz'" + exit 1 + fi + echo "Importing signing key" + wget -q -O - https://repo.i2pd.xyz/r4sas.gpg | sudo apt-key --keyring /etc/apt/trusted.gpg.d/i2pd.gpg add - + echo "Adding APT repository" + echo "deb https://repo.i2pd.xyz/$DIST $RELEASE main" | sudo tee /etc/apt/sources.list.d/i2pd.list + echo "deb-src https://repo.i2pd.xyz/$DIST $RELEASE main" | sudo tee -a /etc/apt/sources.list.d/i2pd.list +} + +function bitcoinI2Pstatus { + echo "# I2P related logs from the bitcoin debug log" + echo "sudo tail -n 200 ${bitcoinLogPath} | grep i2p" + echo + sudo cat ${bitcoinLogPath} | grep i2p + echo + echo "# Running the command:" + echo "sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli -netinfo 4" + echo + sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli -netinfo 4 + echo + echo "# i2pd webconsole:" + localip=$(hostname -I | awk '{print $1}') + echo "http://${localip}:7070" + echo "# Username: i2pd" + echo "# Password: your passwordB" + echo +} + +echo "# Running: 'install.i2pd.sh $*'" +source /home/joinmarket/joinin.conf + +bitcoinConfPath="/home/bitcoin/.bitcoin/bitcoin.conf" +bitcoinLogPath="/home/bitcoin/.bitcoin/debug.log" + +# make sure to be present in PATH +if ! echo "$PATH" | grep "/usr/sbin" >/dev/null; then + export PATH=$PATH:/usr/sbin + echo "PATH=\$PATH:/usr/sbin" | sudo tee -a /etc/profile +fi + +if [ "$1" = "install" ]; then + + isInstalled=$(sudo systemctl list-unit-files | grep -c i2pd) + if [ "${isInstalled}" != "0" ]; then + echo "# i2pd is already installed." + else + echo "# Installing i2pd ..." + + add_repo + + sudo apt-get update + sudo apt-get install -y i2pd + + fi + exit 0 +fi + +if [ "$1" = "1" ] || [ "$1" = "on" ]; then + + isInstalled=$(sudo systemctl list-unit-files | grep -c i2pd) + if [ "${isInstalled}" != "0" ]; then + echo "# i2pd is installed." + else + /home/joinmarket/standalone/install.i2pd.sh install + fi + + if systemctl is-active --quiet i2pd.service; then + echo "# i2pd.service is already active." + else + echo "# sudo systemctl enable i2pd" + sudo systemctl enable i2pd + fi + + echo "# i2pd config" + /home/joinmarket/set.value.sh set debug tor ${bitcoinConfPath} noquotes + confAdd debug i2p ${bitcoinConfPath} + /home/joinmarket/set.value.sh set i2psam 127.0.0.1:7656 ${bitcoinConfPath} noquotes + /home/joinmarket/set.value.sh set i2pacceptincoming 1 ${bitcoinConfPath} noquotes + /home/joinmarket/set.value.sh set onlynet onion ${bitcoinConfPath} noquotes + confAdd onlynet i2p ${bitcoinConfPath} + PASSWORD_B=$(sudo cat ${bitcoinConfPath} | grep rpcpassword | cut -c 13-) + cat </dev/null + sleep 10 + + if i2pd --version; then + echo "# Installed i2pd" + else + echo "# i2pd is not installed" + exit 1 + fi + + # setting value in raspiblitz.conf + /home/joinmarket/set.value.sh set i2pd "on" + + localip=$(hostname -I | awk '{print $1}') + echo "# Config: /etc/i2pd/i2pd.conf" + echo "# i2pd web console: ${localip}:7070" + echo "# Monitor i2p in bitcoind:" + echo "sudo tail -n 100 ${bitcoinLogPath} | grep i2p" + echo "sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli -netinfo 4" + + exit 0 +fi + +if [ "$1" = "addseednodes" ]; then + + if ! sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli -netinfo 4 | grep i2p; then + echo "# i2pd is not running" + /home/joinmarket/standalone/install.i2pd.sh on + fi + + echo "Add 21 randomly selected I2P seed nodes from: https://github.com/bitcoin/bitcoin/blob/master/contrib/seeds/nodes_main.txt" + echo "Monitor in a new terminal with:" + echo "watch sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli -netinfo 4" + echo "This will take some time ..." + + # Fetch and filter the list of seed nodes + i2pSeedNodeList=$(curl -sS https://raw.githubusercontent.com/bitcoin/bitcoin/master/contrib/seeds/nodes_main.txt | grep .b32.i2p:0) + + # Shuffle the list and pick the first 21 nodes + selectedNodes=$(echo "$i2pSeedNodeList" | shuf | head -n 21) + + # Add each selected node + for i2pSeedNode in ${selectedNodes}; do + echo "# Add i2p seed node: ${i2pSeedNode} by running:" + echo "bitcoin-cli addnode $i2pSeedNode onetry" + sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli addnode "$i2pSeedNode" "onetry" + done + + echo + echo "# Display sudo tail -n 100 ${bitcoinLogPath} | grep i2p" + sudo tail -n 100 ${bitcoinLogPath} | grep i2p + echo + echo "# Display bitcoin-cli -netinfo 4" + sudo -u bitcoin /home/bitcoin/bitcoin/bitcoin-cli -netinfo 4 + + exit 0 +fi + +# switch off +if [ "$1" = "0" ] || [ "$1" = "off" ]; then + + echo "# stop & remove systemd service" + sudo systemctl stop i2pd 2>/dev/null + sudo systemctl disable i2pd.service + + echo "# Uninstall with apt" + sudo apt remove -y i2pd + + echo "# Remove settings from bitcoind" + /home/joinmarket/set.value.sh delete debug ${bitcoinConfPath} noquotes + /home/joinmarket/set.value.sh set debug tor ${bitcoinConfPath} noquotes + /home/joinmarket/set.value.sh delete i2psam ${bitcoinConfPath} noquotes + /home/joinmarket/set.value.sh delete i2pacceptincoming ${bitcoinConfPath} noquotes + /home/joinmarket/set.value.sh delete onlynet ${bitcoinConfPath} noquotes + /home/joinmarket/set.value.sh set onlynet onion ${bitcoinConfPath} noquotes + + sudo rm /etc/systemd/system/i2pd.service + + sudo ufw delete allow 7070 + + if ! i2pd --version 2>/dev/null; then + echo "# OK - i2pd is not installed now" + else + echo "# i2pd is still installed" + exit 1 + fi + + # setting value in raspiblitz.conf + /home/joinmarket/set.value.sh set i2pd "off" + + exit 0 +fi + +if [ "$1" = "status" ]; then + bitcoinI2Pstatus + exit 0 +fi + +echo "# FAIL - Unknown Parameter $1" +exit 1 From b9360c1008519275c69c403ddd0a8f1612806ff2 Mon Sep 17 00:00:00 2001 From: openoms Date: Mon, 15 Jul 2024 17:48:39 +0200 Subject: [PATCH 29/56] docs: clarify jam remote steps --- scripts/jam-remote/README.md | 47 ++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/scripts/jam-remote/README.md b/scripts/jam-remote/README.md index ed49029..9a73427 100644 --- a/scripts/jam-remote/README.md +++ b/scripts/jam-remote/README.md @@ -1,33 +1,38 @@ # Install Jam and connect to a remote Joininbox -* tested on Debian Bullseye desktop - Ubuntu should also work +* tested on Debian Bullseye and Bookworm desktop - Ubuntu should also work -### Download the repo -``` -git clone https://github.com/openoms/joininbox -cd joininbox -``` - -### Install Jam locally -* will be under the user: `jam` -``` -cd scripts/jam-remote -bash install.jam.sh on -``` - -### On your Joininbox +## Steps in your Joininbox terminal * (optional) update the joininbox scripts: `UPDATE` - `ADVANCED` - `JBCOMMIT` * start the `API` from `TOOLS` * (optional) start the ob-watcher from `OFFERS` +## Steps on your desktop +### Download the repo +* move to a directory where the joininbox repo will be stored + ``` + git clone https://github.com/openoms/joininbox + cd joininbox + ``` + +### Install Jam locally +* will be under the user: `jam` + ``` + cd scripts/jam-remote + bash install.jam.sh on + ``` + ### Forward the API and ob-watcher ports with ssh from your Joininbox -``` -bash ssh-portforward $JOININBOX_LAN_IP -``` +* run the ssh-port-forward script in the `joininbox/scripts/jam-remote` folder + ``` + bash ./ssh-port-forward.sh $JOININBOX_LAN_IP + ``` * leave this terminal open until working with Jam * close when done to close the ssh connection -* Earn will continue to run on the Joininbox -* can check it's logs in the menu - `MAKER` - `LOGS` ### Open Jam locally using the wallets on your remote Joininbox -`https://localhost:7501` +* open Jam at https://localhost:7501 +* accept the self-signed certificate served from your Joininbox +* Use Jam - docs: https://jamdocs.org/ +* Earn will continue to run on the Joininbox even after the terminal and Jam windows are closed +* can check the logs in the Joininbox menu - `MAKER` - `LOGS` From 6b606760eb900fe797ec77cdaa17c6aecbd07330 Mon Sep 17 00:00:00 2001 From: openoms Date: Fri, 13 Sep 2024 10:58:56 +0200 Subject: [PATCH 30/56] correct menu option name --- scripts/menu.yg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/menu.yg.sh b/scripts/menu.yg.sh index a48929a..dbdfcc4 100755 --- a/scripts/menu.yg.sh +++ b/scripts/menu.yg.sh @@ -73,7 +73,7 @@ case $CHOICE in --msgbox " There are no stats because the Yield Generator was never run. -Start with the menu option: RUN_YG" 10 50 +Start with the menu option: MAKER" 10 50 else dialog --prgbox "/home/joinmarket/info.stats.sh showAllEarned" 9 55 fi;; From c75ddacfc955a77fc44749f4eb1c6b429fddd1ab Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:32:28 +0300 Subject: [PATCH 31/56] update amd64 base image to debian-12.7.0-amd64-netinst.iso (#157) --- ci/amd64/debian/joininbox-amd64-debian.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index acaafb3..da81573 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -1,7 +1,7 @@ { "variables": { - "iso_name": "debian-12.6.0-amd64-netinst.iso", - "iso_checksum": "ade3a4acc465f59ca2496344aab72455945f3277a52afc5a2cae88cdc370fa12", + "iso_name": "debian-12.7.0-amd64-netinst.iso", + "iso_checksum": "8fde79cfc6b20a696200fc5c15219cf6d721e8feb367e9e0e33a79d1cb68fa83", "box_basename": "debian", "build_directory": "../builds", "headless": "false", From aaa22e375bf962ee251b3f1c2085e38da79fa8d5 Mon Sep 17 00:00:00 2001 From: Arman The Parman <77603167+ArmanTheParman@users.noreply.github.com> Date: Sat, 23 Nov 2024 00:43:48 +1100 Subject: [PATCH 32/56] smol typo (#159) --- scripts/start.joininbox.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/start.joininbox.sh b/scripts/start.joininbox.sh index fd8dfa6..ad297a6 100755 --- a/scripts/start.joininbox.sh +++ b/scripts/start.joininbox.sh @@ -179,5 +179,5 @@ fi localip=$(hostname -I | awk '{print $1}') sed -i "s#^localip=.*#localip=$localip#g" $joininConfPath -# change the onion_serving_port if LND is present (avoid collusion with LND REST port) +# change the onion_serving_port if LND is present (avoid collision with LND REST port) sed -i "s#^onion_serving_port = 8080#onion_serving_port = 8090#g" $JMcfgPath From 18d28bedf46bea88d8a931a7f2502102470299f9 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Fri, 22 Nov 2024 21:46:59 +0100 Subject: [PATCH 33/56] amd64 base image update to debian-12.8.0 (#160) --- ci/amd64/debian/joininbox-amd64-debian.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index da81573..b7ed82f 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -1,7 +1,7 @@ { "variables": { - "iso_name": "debian-12.7.0-amd64-netinst.iso", - "iso_checksum": "8fde79cfc6b20a696200fc5c15219cf6d721e8feb367e9e0e33a79d1cb68fa83", + "iso_name": "debian-12.8.0-amd64-netinst.iso", + "iso_checksum": "04396d12b0f377958a070c38a923c227832fa3b3e18ddc013936ecf492e9fbb3", "box_basename": "debian", "build_directory": "../builds", "headless": "false", From 7800d632a5822dc16c80810ea515cc748c01d7ca Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sat, 23 Nov 2024 08:00:30 +0100 Subject: [PATCH 34/56] Tweak logs and stats display (#162) * stats display tweak * maximize dialog prgbox views for logs --- scripts/_commands.sh | 2 +- scripts/_functions.menu.sh | 2 +- scripts/menu.orderbook.sh | 2 +- scripts/menu.yg.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/_commands.sh b/scripts/_commands.sh index fa42c23..116a9a3 100755 --- a/scripts/_commands.sh +++ b/scripts/_commands.sh @@ -64,7 +64,7 @@ function torthistx() { # command: stats # shows the uptime and the fees earned as a Maker function stats() { - /home/joinmarket/info.stats.sh + /home/joinmarket/info.stats.sh showAllEarned } # command: qtgui diff --git a/scripts/_functions.menu.sh b/scripts/_functions.menu.sh index a4b1519..aa6d247 100644 --- a/scripts/_functions.menu.sh +++ b/scripts/_functions.menu.sh @@ -37,7 +37,7 @@ function menu_MAKER() { sleep 3 dialog \ --title "Monitoring the Yield Generator - press CTRL+C to exit" \ - --prgbox "sudo journalctl -fn20 -u yg-privacyenhanced" 30 200 + --prgbox "sudo journalctl -fn100 -u yg-privacyenhanced" -1 -1 echo "# returning to the menu..." sleep 1 /home/joinmarket/menu.yg.sh diff --git a/scripts/menu.orderbook.sh b/scripts/menu.orderbook.sh index 6c09bfd..952fa32 100755 --- a/scripts/menu.orderbook.sh +++ b/scripts/menu.orderbook.sh @@ -90,7 +90,7 @@ function startOrderBook() { sleep 3 dialog \ --title "Monitoring the ob-watcher - press CTRL+C to exit" \ - --prgbox "sudo journalctl -fn20 -u ob-watcher" 30 200 + --prgbox "sudo journalctl -fn100 -u ob-watcher" -1 -1 } if [ "$1" = startOrderBookService ]; then diff --git a/scripts/menu.yg.sh b/scripts/menu.yg.sh index dbdfcc4..73fa9f5 100755 --- a/scripts/menu.yg.sh +++ b/scripts/menu.yg.sh @@ -75,7 +75,7 @@ There are no stats because the Yield Generator was never run. Start with the menu option: MAKER" 10 50 else - dialog --prgbox "/home/joinmarket/info.stats.sh showAllEarned" 9 55 + dialog --prgbox "/home/joinmarket/info.stats.sh showAllEarned" 9 67 fi;; NICKNAME) name=$(YGnickname) From b532dbeab345c64f5a63807b147c8dcf11064965 Mon Sep 17 00:00:00 2001 From: openoms Date: Mon, 2 Dec 2024 13:40:47 +0100 Subject: [PATCH 35/56] docs: remove 32 bit SBC support --- FAQ.md | 52 +++------------------------------------------------- README.md | 2 +- 2 files changed, 4 insertions(+), 50 deletions(-) diff --git a/FAQ.md b/FAQ.md index 26bcfc2..9d2169a 100644 --- a/FAQ.md +++ b/FAQ.md @@ -10,7 +10,6 @@ - [Log in through SSH using a hardware wallet](#log-in-through-ssh-using-a-hardware-wallet) - [SSH through Tor from Linux](#ssh-through-tor-from-linux) - [Allow Tor to connect to localhost](#allow-tor-to-connect-to-localhost) -- [Set up Armbian on the Hardkernel Odroid HC1 / XU4](#set-up-armbian-on-the-hardkernel-odroid-hc1--xu4) - [Download and verify Raspbian SDcard image for a Raspberry Pi](#download-and-verify-raspbian-sdcard-image-for-a-raspberry-pi) - [Error when connecting to a full node remotely through Tor](#error-when-connecting-to-a-full-node-remotely-through-tor) - [Erase the joinmarket user and the /home/joinmarket folder](#erase-the-joinmarket-user-and-the-homejoinmarket-folder) @@ -172,55 +171,10 @@ Use `ssh` with `torsocks` on the desktop (needs Tor installed): * Restart Tor: `sudo systemctl restart tor` -## Set up Armbian on the Hardkernel Odroid HC1 / XU4 -* Download the base image (`.img.gz`), the `.sha` and `.asc` file -https://www.armbian.com/odroid-xu4/ -* Verify: https://docs.armbian.com/User-Guide_Getting-Started/#how-to-check-download-authenticity - ```bash - gpg --keyserver ha.pool.sks-keyservers.net --recv-key DF00FAF1C577104B50BF1D0093D6889F9F0E78D5 - # gpg: key 93D6889F9F0E78D5: public key "Igor Pecovnik # " imported - # gpg: Total number processed: 1 - # gpg: imported: 1 - gpg --verify Armbian_21.02.3_Odroidxu4_buster_legacy_4.14.222.img.xz.asc - # gpg: assuming signed data in 'Armbian_21.02.3_Odroidxu4_buster_legacy_4.14.222.img.xz' - # gpg: Signature made Tue 09 Mar 2021 03:00:30 GMT - # gpg: using RSA key DF00FAF1C577104B50BF1D0093D6889F9F0E78D5 - # gpg: Good signature from "Igor Pecovnik " [unknown] - # gpg: aka "Igor Pecovnik (Ljubljana, Slovenia) " [unknown] - # gpg: WARNING: This key is not certified with a trusted signature! - # gpg: There is no indication that the signature belongs to the owner. - # Primary key fingerprint: DF00 FAF1 C577 104B 50BF 1D00 93D6 889F 9F0E 78D5 - shasum -c Armbian_21.02.3_Odroidxu4_buster_legacy_4.14.222.img.xz.sha - # Armbian_21.02.3_Odroidxu4_buster_legacy_4.14.222.img.xz: OK - ``` -* Preparation - Make sure you have a good & reliable SD card and a proper power supply. Archives can be uncompressed with 7-Zip on Windows, Keka on OS X and 7z on Linux (apt-get install p7zip-full). RAW images can be written with Etcher (all OS). -* Boot - Insert the SD card into the slot, connect a cable to your network if possible or a display and power your board. (First) boot (with DHCP) takes up to 35 seconds with a class 10 SD Card. -* Login - Log in as: `root` Password: `1234`. Then you are prompted to change this password (US-Keyboard setting). When done, you are asked to create a normal user-account for your everyday tasks. -* Change the password. -* Create a new user called `joinmarket` and set the password (the password will be changed to `joininbox`). - Keep pressing [ENTER] to use the default user information. -* Continue to [install JoininBox](README.md#install-joininbox) - ## Download and verify Raspbian SDcard image for a Raspberry Pi -To be able to open the JoinMarket-QT GUI on the desktop from the RPI -need to use the Raspberry Pi OS (32-bit) with desktop image -* Download image: -https://downloads.raspberrypi.org/raspios_armhf/images/raspios_armhf-2020-05-28/2020-05-27-raspios-buster-armhf.zip -* Download signature: -https://downloads.raspberrypi.org/raspios_armhf/images/raspios_armhf-2020-05-28/2020-05-27-raspios-buster-armhf.zip.sig -* Import PGP pubkey: -`curl https://www.raspberrypi.org/raspberrypi_downloads.gpg.key | gpg --import` -* Verify the image: -`gpg --verify 2020-05-27-raspios-buster-armhf.zip.sig` -* Flash the image to an SDcard, can use the [Raspberry Pi Imager](https://www.raspberrypi.org/downloads/) -* put a file called simply: `ssh` to the root of the sdcard. -Read more on [how to gain ssh access here](https://www.raspberrypi.org/documentation/remote-access/ssh/). -* boot up the RPi and log in with ssh to: -`pi@LAN_IP_ADDRESS` -The default password is: `raspberry` +* Download [Raspberry Pi Imager](https://www.raspberrypi.com/software/) +* enable the ssh login in the +* Flash the image to a min 32GB Endurance type SDcard * Continue to [install JoininBox](README.md#install-joininbox) ## Error when connecting to a full node remotely through Tor diff --git a/README.md b/README.md index 3588164..3aa7a3b 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Recommended minimum: ### Install JoininBox * Start as the `root` user or change with: -`$ sudo - su` +`$ sudo su -` * Run the [build script](https://github.com/openoms/joininbox/blob/master/build_joininbox.sh): ```bash From 41a5c9e573feb2d5e51c852e8629d2f6236e54fb Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:43:28 +0100 Subject: [PATCH 36/56] fix standalone bitcoind service (allow reach home folder) (#163) --- scripts/standalone/_functions.standalone.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/standalone/_functions.standalone.sh b/scripts/standalone/_functions.standalone.sh index 207486f..a4108cb 100755 --- a/scripts/standalone/_functions.standalone.sh +++ b/scripts/standalone/_functions.standalone.sh @@ -250,8 +250,6 @@ StandardError=journal PrivateTmp=true # Mount /usr, /boot/ and /etc read-only for the process. ProtectSystem=full -# Deny access to /home, /root and /run/user -ProtectHome=true # Disallow the process and all of its children to gain # new privileges through execve(). NoNewPrivileges=true From 8afc78dcf447d4e4bac69dc8ab90526b38e1eba6 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 13 Jan 2025 23:36:29 +0100 Subject: [PATCH 37/56] add pip config set global.break-system-packages true to jm install (#164) --- scripts/install.joinmarket.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/install.joinmarket.sh b/scripts/install.joinmarket.sh index 9ba1b90..928848e 100755 --- a/scripts/install.joinmarket.sh +++ b/scripts/install.joinmarket.sh @@ -159,6 +159,7 @@ function installJoinMarket() { cd /home/${user} || exit 1 # https://github.com/JoinMarket-Org/joinmarket-clientserver/issues/668#issuecomment-717815719 sudo apt-get install -y build-essential automake pkg-config libffi-dev python3-dev + sudo -u ${user} pip config set global.break-system-packages true sudo -u ${user} pip install libtool asn1crypto cffi pycparser echo "# Installing JoinMarket" From 19ffa45e66415814f75423ea897be2918e823344 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 13 Jan 2025 23:37:24 +0100 Subject: [PATCH 38/56] add 64bit Raspberry Pi OS Lite base image for RPi5 (#161) --- .github/workflows/amd64-image-build.yml | 4 + .github/workflows/arm64-rpi-image-build.yml | 4 + .github/workflows/arm64-rpi5-image-build.yml | 81 ++++++++++++++++++++ Makefile | 17 ++++ ci/amd64/packer.build.amd64-debian.sh | 2 +- ci/arm64-rpi/arm64-rpi5.pkr.hcl | 60 +++++++++++++++ ci/arm64-rpi/arm64-rpi5.sh | 20 +++++ 7 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/arm64-rpi5-image-build.yml create mode 100644 ci/arm64-rpi/arm64-rpi5.pkr.hcl create mode 100644 ci/arm64-rpi/arm64-rpi5.sh diff --git a/.github/workflows/amd64-image-build.yml b/.github/workflows/amd64-image-build.yml index f0524bc..932e67c 100644 --- a/.github/workflows/amd64-image-build.yml +++ b/.github/workflows/amd64-image-build.yml @@ -1,5 +1,9 @@ name: amd64-image-build +concurrency: + group: amd64-image-build-${{ github.head_ref }} + cancel-in-progress: true + on: push: branches: [ "master" ] diff --git a/.github/workflows/arm64-rpi-image-build.yml b/.github/workflows/arm64-rpi-image-build.yml index 66640fb..6f05d40 100644 --- a/.github/workflows/arm64-rpi-image-build.yml +++ b/.github/workflows/arm64-rpi-image-build.yml @@ -1,5 +1,9 @@ name: arm64-rpi-image-build +concurrency: + group: arm64-rpi-image-build-${{ github.head_ref }} + cancel-in-progress: true + on: push: branches: [ "master" ] diff --git a/.github/workflows/arm64-rpi5-image-build.yml b/.github/workflows/arm64-rpi5-image-build.yml new file mode 100644 index 0000000..dba0e8e --- /dev/null +++ b/.github/workflows/arm64-rpi5-image-build.yml @@ -0,0 +1,81 @@ +name: arm64-rpi5-image-build + +concurrency: + group: arm64-rpi5-image-build-${{ github.head_ref }} + cancel-in-progress: true + +on: + push: + branches: [ "master" ] + paths-ignore: + - '*.md' + - '.gitignore' + - 'images' + - 'ci/amd64/**' + - 'scripts/jam-remote' + - 'Makefile' + pull_request: + branches: [ "master" ] + paths-ignore: + - '*.md' + - '.gitignore' + - 'images' + - 'ci/amd64/**' + - 'scripts/jam-remote' + - 'Makefile' + workflow_dispatch: + +jobs: + arm64-rpi5-image-build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + + - name: Set values + id: set_values + run: | + echo "BUILD_DATE=$(date +"%Y-%m-%d")" >> $GITHUB_ENV + echo "BUILD_VERSION=$(git describe --always --tags)" >> $GITHUB_ENV + if [[ "${{github.event_name}}" == "pull_request" ]]; then + echo "::set-output name=github_user::${{github.event.pull_request.head.repo.owner.login}}" + else + echo "::set-output name=github_user::$(echo ${{github.repository}} | cut -d'/' -f1)" + fi + + - name: Display the build name + run: echo "Building the joininbox-arm64-rpi5-image-${{ env.BUILD_DATE }}-${{ env.BUILD_VERSION }}" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + with: + platforms: linux/amd64,linux/arm64 + + - name: Run the build script + run: | + echo "Running with: ${{steps.set_values.outputs.github_user}} $GITHUB_HEAD_REF" + cd ci/arm64-rpi + bash arm64-rpi5.sh ${{steps.set_values.outputs.github_user}} $GITHUB_HEAD_REF + + - name: Compute checksum of the raw image + run: | + cd ci/arm64-rpi + sha256sum joininbox-arm64-rpi5.img > joininbox-arm64-rpi5.img.sha256 + + - name: Compress image + run: | + cd ci/arm64-rpi + gzip -v9 joininbox-arm64-rpi5.img + + - name: Compute checksum of the compressed image + run: | + cd ci/arm64-rpi + sha256sum joininbox-arm64-rpi5.img.gz > joininbox-arm64-rpi5.img.gz.sha256 + + - name: Upload the image and checksums + uses: actions/upload-artifact@v3 + with: + name: joininbox-arm64-rpi5-image-${{ env.BUILD_DATE }}-${{ env.BUILD_VERSION }} + path: | + ${{ github.workspace }}/ci/arm64-rpi/joininbox-arm64-rpi5.img.sha256 + ${{ github.workspace }}/ci/arm64-rpi/joininbox-arm64-rpi5.img.gz + ${{ github.workspace }}/ci/arm64-rpi/joininbox-arm64-rpi5.img.gz.sha256 diff --git a/Makefile b/Makefile index f26dc65..c34acd8 100644 --- a/Makefile +++ b/Makefile @@ -35,3 +35,20 @@ arm64-rpi-image: # Compute checksum of the compressed image cd ci/arm64-rpi && \ sha256sum joininbox-arm64-rpi.img.gz > joininbox-arm64-rpi.img.gz.sha256 + +arm64-rpi5-image: + # build image + cd ci/arm64-rpi && \ + bash arm64-rpi5.sh $(GITHUB_USER) $(CURRENT_BRANCH) + + # Compute checksum of the raw image + cd ci/arm64-rpi && \ + sha256sum joininbox-arm64-rpi5.img > joininbox-arm64-rpi5.img.sha256 + + # Compress image + cd ci/arm64-rpi && \ + gzip -v9 joininbox-arm64-rpi5.img + + # Compute checksum of the compressed image + cd ci/arm64-rpi && \ + sha256sum joininbox-arm64-rpi5.img.gz > joininbox-arm64-rpi5.img.gz.sha256 diff --git a/ci/amd64/packer.build.amd64-debian.sh b/ci/amd64/packer.build.amd64-debian.sh index 8da4c62..1970b41 100644 --- a/ci/amd64/packer.build.amd64-debian.sh +++ b/ci/amd64/packer.build.amd64-debian.sh @@ -3,7 +3,7 @@ # 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 "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" + 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 diff --git a/ci/arm64-rpi/arm64-rpi5.pkr.hcl b/ci/arm64-rpi/arm64-rpi5.pkr.hcl new file mode 100644 index 0000000..dcdd912 --- /dev/null +++ b/ci/arm64-rpi/arm64-rpi5.pkr.hcl @@ -0,0 +1,60 @@ +variable "github_user" {} +variable "branch" {} + +source "arm" "joininbox-arm64-rpi5" { + file_checksum_type = "sha256" + file_checksum = "6ac3a10a1f144c7e9d1f8e568d75ca809288280a593eb6ca053e49b539f465a4" + file_target_extension = "xz" + file_unarchive_cmd = ["xz", "--decompress", "$ARCHIVE_PATH"] + file_urls = ["https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz"] + image_build_method = "resize" + image_chroot_env = ["PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin"] + image_partitions { + filesystem = "vfat" + mountpoint = "/boot" + name = "boot" + size = "256M" + start_sector = "8192" + type = "c" + } + image_partitions { + filesystem = "ext4" + mountpoint = "/" + name = "root" + size = "0" + start_sector = "532480" + type = "83" + } + image_path = "joininbox-arm64-rpi5.img" + image_size = "8G" + image_type = "dos" + qemu_binary_destination_path = "/usr/bin/qemu-aarch64-static" + qemu_binary_source_path = "/usr/bin/qemu-aarch64-static" +} + +build { + sources = ["source.arm.joininbox-arm64-rpi5"] + + provisioner "shell" { + inline = [ + "echo 'nameserver 1.1.1.1' >/etc/resolv.conf", + "echo 'nameserver 8.8.8.8' >>/etc/resolv.conf", + "echo $(hostname -I | awk '{print $1}') $(hostname) >>/etc/hosts", + "echo 127.0.1.1 $(hostname) >>/etc/hosts", + "echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections", + "apt-get update", + "apt-get install -y sudo wget", + "apt-get -y autoremove", + "apt-get -y clean", + ] + } + + provisioner "shell" { + environment_vars = [ + "github_user=${var.github_user}", + "branch=${var.branch}" + ] + script = "./joininbox.sh" + } + +} diff --git a/ci/arm64-rpi/arm64-rpi5.sh b/ci/arm64-rpi/arm64-rpi5.sh new file mode 100644 index 0000000..6f0a13b --- /dev/null +++ b/ci/arm64-rpi/arm64-rpi5.sh @@ -0,0 +1,20 @@ +#!/bin/bash -e + +if [ $# -gt 0 ]; then + github_user=$1 +else + github_user=openoms +fi + +if [ $# -gt 1 ]; then + branch=$2 +else + branch=master +fi + +# Build the image in docker +echo -e "\nBuild Packer image..." +# from https://hub.docker.com/r/mkaczanowski/packer-builder-arm/tags +docker run --rm --privileged -v /dev:/dev -v ${PWD}:/build \ + mkaczanowski/packer-builder-arm:1.0.4@sha256:df09a8e249a292f10ca9b8cfd73420f5b987b6ac337d4ef28b6f4a8e61118822 \ + build -var "github_user=${github_user}" -var "branch=${branch}" arm64-rpi5.pkr.hcl From 601eede8b51af5a22ec87f8269ae68fce05a478e Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:48:15 +0100 Subject: [PATCH 39/56] amd64 base image update to debian-12.9.0-amd64-netinst.iso (#165) --- ci/amd64/debian/joininbox-amd64-debian.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index b7ed82f..1a4427c 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -1,7 +1,7 @@ { "variables": { - "iso_name": "debian-12.8.0-amd64-netinst.iso", - "iso_checksum": "04396d12b0f377958a070c38a923c227832fa3b3e18ddc013936ecf492e9fbb3", + "iso_name": "debian-12.9.0-amd64-netinst.iso", + "iso_checksum": "1257373c706d8c07e6917942736a865dfff557d21d76ea3040bb1039eb72a054", "box_basename": "debian", "build_directory": "../builds", "headless": "false", From 44e095e3245e88ce4c9e9f2b6c5d84fd1911e58c Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Sun, 25 May 2025 23:30:11 +0200 Subject: [PATCH 40/56] fix: identify new location of raspiblitz.conf file (#168) --- scripts/start.joininbox.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/start.joininbox.sh b/scripts/start.joininbox.sh index ad297a6..0aec7f8 100755 --- a/scripts/start.joininbox.sh +++ b/scripts/start.joininbox.sh @@ -23,7 +23,7 @@ if [ "$setupStep" -lt 100 ]; then # identify running env runningEnvEntry=$(grep -c "runningEnv" <$joininConfPath) if [ "$runningEnvEntry" -eq 0 ]; then - if [ -f "/mnt/hdd/raspiblitz.conf" ]; then + if [ -f "/mnt/hdd/raspiblitz.conf" ] || [ -f "/mnt/hdd/app-data/raspiblitz.conf" ] ; then runningEnv="raspiblitz" elif [ -f "/usr/share/mynode/mynode_config.sh" ]; then runningEnv="mynode" From 9594fbf20e83a848d85521d73d4beccd60f5c580 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 26 May 2025 00:08:35 +0200 Subject: [PATCH 41/56] jam-remote update to 0.3.0 (#169) --- scripts/jam-remote/install.jam.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/jam-remote/install.jam.sh b/scripts/jam-remote/install.jam.sh index 449d5c3..9740857 100644 --- a/scripts/jam-remote/install.jam.sh +++ b/scripts/jam-remote/install.jam.sh @@ -3,15 +3,15 @@ # https://github.com/joinmarket-webui/jam USERNAME=jam -WEBUI_VERSION="v0.2.0" +WEBUI_VERSION="v0.3.0" REPO=joinmarket-webui/jam HOME_DIR=/home/${USERNAME} APP_DIR=webui SOURCEDIR=$(pwd) -PGPsigner="dergigi" +PGPsigner="theborakompanioni" PGPpubkeyLink="https://github.com/${PGPsigner}.gpg" -PGPpubkeyFingerprint="89C4A25E69A5DE7F" +PGPpubkeyFingerprint="E8070AF0053AAC0D" # command info if [ $# -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "-help" ]; then From b5bc21ad7bc9ccc232bf1d83a04eee19848e26a5 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 26 May 2025 00:26:11 +0200 Subject: [PATCH 42/56] jam-remote re-activate code signing check (#170) * jam-remote re-activate code signing check * chore(install.selfsignedcert.sh): non-interactive script --- scripts/jam-remote/install.jam.sh | 4 ++-- scripts/jam-remote/install.selfsignedcert.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/jam-remote/install.jam.sh b/scripts/jam-remote/install.jam.sh index 9740857..8b015f1 100644 --- a/scripts/jam-remote/install.jam.sh +++ b/scripts/jam-remote/install.jam.sh @@ -59,8 +59,8 @@ if [ "$1" = "on" ]; then cd jam || exit 1 sudo -u $USERNAME git reset --hard ${WEBUI_VERSION} - #sudo -u $USERNAME bash ${SOURCEDIR}/../verify.git.sh \ - # "${PGPsigner}" "${PGPpubkeyLink}" "${PGPpubkeyFingerprint}" "v${WEBUI_VERSION}" || exit 1 + sudo -u $USERNAME bash ${SOURCEDIR}/../verify.git.sh \ + "${PGPsigner}" "${PGPpubkeyLink}" "${PGPpubkeyFingerprint}" "${WEBUI_VERSION}" || exit 1 cd $HOME_DIR || exit 1 sudo -u $USERNAME mv jam $APP_DIR diff --git a/scripts/jam-remote/install.selfsignedcert.sh b/scripts/jam-remote/install.selfsignedcert.sh index ebf40e5..ffc6b53 100644 --- a/scripts/jam-remote/install.selfsignedcert.sh +++ b/scripts/jam-remote/install.selfsignedcert.sh @@ -3,10 +3,10 @@ USERNAME=jam -sudo apt install nginx +sudo apt-get install -y nginx if ! sudo ls /home/jam/nginx/tls.cert || ! sudo ls /home/jam/nginx/tls.key; then - sudo apt-get install openssl + sudo apt-get install -y openssl subj="/C=US/ST=Utah/L=Lehi/O=Your Company, Inc./OU=IT/CN=example.com" sudo -u $USERNAME mkdir -p /home/jam/nginx/ \ From 304144e881bd03dd89b6f7a5dfea0dbaec1eefa5 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Mon, 26 May 2025 20:03:49 +0200 Subject: [PATCH 43/56] amd64 and arm64 base image updates (#167) * amd64 base image update to debian-12.11.0-amd64-netinst.iso * chore: update actions/upload-artifact to v4 * chore: build arm64-rpi on raspios_lite_arm64 only --- .github/workflows/amd64-image-build.yml | 2 +- .github/workflows/arm64-rpi-image-build.yml | 2 +- .github/workflows/arm64-rpi5-image-build.yml | 81 -------------------- Makefile | 17 ---- ci/amd64/debian/joininbox-amd64-debian.json | 4 +- ci/arm64-rpi/arm64-rpi.pkr.hcl | 4 +- ci/arm64-rpi/arm64-rpi5.pkr.hcl | 60 --------------- ci/arm64-rpi/arm64-rpi5.sh | 20 ----- 8 files changed, 6 insertions(+), 184 deletions(-) delete mode 100644 .github/workflows/arm64-rpi5-image-build.yml delete mode 100644 ci/arm64-rpi/arm64-rpi5.pkr.hcl delete mode 100644 ci/arm64-rpi/arm64-rpi5.sh diff --git a/.github/workflows/amd64-image-build.yml b/.github/workflows/amd64-image-build.yml index 932e67c..54b97a0 100644 --- a/.github/workflows/amd64-image-build.yml +++ b/.github/workflows/amd64-image-build.yml @@ -65,7 +65,7 @@ jobs: sha256sum joininbox-amd64-debian.qcow2.gz > joininbox-amd64-debian.qcow2.gz.sha256 - name: Upload the image and checksums - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: joininbox-amd64-image-${{ env.BUILD_DATE }}-${{ env.BUILD_VERSION }} path: | diff --git a/.github/workflows/arm64-rpi-image-build.yml b/.github/workflows/arm64-rpi-image-build.yml index 6f05d40..137b2da 100644 --- a/.github/workflows/arm64-rpi-image-build.yml +++ b/.github/workflows/arm64-rpi-image-build.yml @@ -72,7 +72,7 @@ jobs: sha256sum joininbox-arm64-rpi.img.gz > joininbox-arm64-rpi.img.gz.sha256 - name: Upload the image and checksums - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: joininbox-arm64-rpi-image-${{ env.BUILD_DATE }}-${{ env.BUILD_VERSION }} path: | diff --git a/.github/workflows/arm64-rpi5-image-build.yml b/.github/workflows/arm64-rpi5-image-build.yml deleted file mode 100644 index dba0e8e..0000000 --- a/.github/workflows/arm64-rpi5-image-build.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: arm64-rpi5-image-build - -concurrency: - group: arm64-rpi5-image-build-${{ github.head_ref }} - cancel-in-progress: true - -on: - push: - branches: [ "master" ] - paths-ignore: - - '*.md' - - '.gitignore' - - 'images' - - 'ci/amd64/**' - - 'scripts/jam-remote' - - 'Makefile' - pull_request: - branches: [ "master" ] - paths-ignore: - - '*.md' - - '.gitignore' - - 'images' - - 'ci/amd64/**' - - 'scripts/jam-remote' - - 'Makefile' - workflow_dispatch: - -jobs: - arm64-rpi5-image-build: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v3 - - - name: Set values - id: set_values - run: | - echo "BUILD_DATE=$(date +"%Y-%m-%d")" >> $GITHUB_ENV - echo "BUILD_VERSION=$(git describe --always --tags)" >> $GITHUB_ENV - if [[ "${{github.event_name}}" == "pull_request" ]]; then - echo "::set-output name=github_user::${{github.event.pull_request.head.repo.owner.login}}" - else - echo "::set-output name=github_user::$(echo ${{github.repository}} | cut -d'/' -f1)" - fi - - - name: Display the build name - run: echo "Building the joininbox-arm64-rpi5-image-${{ env.BUILD_DATE }}-${{ env.BUILD_VERSION }}" - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - with: - platforms: linux/amd64,linux/arm64 - - - name: Run the build script - run: | - echo "Running with: ${{steps.set_values.outputs.github_user}} $GITHUB_HEAD_REF" - cd ci/arm64-rpi - bash arm64-rpi5.sh ${{steps.set_values.outputs.github_user}} $GITHUB_HEAD_REF - - - name: Compute checksum of the raw image - run: | - cd ci/arm64-rpi - sha256sum joininbox-arm64-rpi5.img > joininbox-arm64-rpi5.img.sha256 - - - name: Compress image - run: | - cd ci/arm64-rpi - gzip -v9 joininbox-arm64-rpi5.img - - - name: Compute checksum of the compressed image - run: | - cd ci/arm64-rpi - sha256sum joininbox-arm64-rpi5.img.gz > joininbox-arm64-rpi5.img.gz.sha256 - - - name: Upload the image and checksums - uses: actions/upload-artifact@v3 - with: - name: joininbox-arm64-rpi5-image-${{ env.BUILD_DATE }}-${{ env.BUILD_VERSION }} - path: | - ${{ github.workspace }}/ci/arm64-rpi/joininbox-arm64-rpi5.img.sha256 - ${{ github.workspace }}/ci/arm64-rpi/joininbox-arm64-rpi5.img.gz - ${{ github.workspace }}/ci/arm64-rpi/joininbox-arm64-rpi5.img.gz.sha256 diff --git a/Makefile b/Makefile index c34acd8..f26dc65 100644 --- a/Makefile +++ b/Makefile @@ -35,20 +35,3 @@ arm64-rpi-image: # Compute checksum of the compressed image cd ci/arm64-rpi && \ sha256sum joininbox-arm64-rpi.img.gz > joininbox-arm64-rpi.img.gz.sha256 - -arm64-rpi5-image: - # build image - cd ci/arm64-rpi && \ - bash arm64-rpi5.sh $(GITHUB_USER) $(CURRENT_BRANCH) - - # Compute checksum of the raw image - cd ci/arm64-rpi && \ - sha256sum joininbox-arm64-rpi5.img > joininbox-arm64-rpi5.img.sha256 - - # Compress image - cd ci/arm64-rpi && \ - gzip -v9 joininbox-arm64-rpi5.img - - # Compute checksum of the compressed image - cd ci/arm64-rpi && \ - sha256sum joininbox-arm64-rpi5.img.gz > joininbox-arm64-rpi5.img.gz.sha256 diff --git a/ci/amd64/debian/joininbox-amd64-debian.json b/ci/amd64/debian/joininbox-amd64-debian.json index 1a4427c..7e1bef0 100644 --- a/ci/amd64/debian/joininbox-amd64-debian.json +++ b/ci/amd64/debian/joininbox-amd64-debian.json @@ -1,7 +1,7 @@ { "variables": { - "iso_name": "debian-12.9.0-amd64-netinst.iso", - "iso_checksum": "1257373c706d8c07e6917942736a865dfff557d21d76ea3040bb1039eb72a054", + "iso_name": "debian-12.11.0-amd64-netinst.iso", + "iso_checksum": "30ca12a15cae6a1033e03ad59eb7f66a6d5a258dcf27acd115c2bd42d22640e8", "box_basename": "debian", "build_directory": "../builds", "headless": "false", diff --git a/ci/arm64-rpi/arm64-rpi.pkr.hcl b/ci/arm64-rpi/arm64-rpi.pkr.hcl index fe27d74..5a1482d 100644 --- a/ci/arm64-rpi/arm64-rpi.pkr.hcl +++ b/ci/arm64-rpi/arm64-rpi.pkr.hcl @@ -3,10 +3,10 @@ variable "branch" {} source "arm" "joininbox-arm64-rpi" { file_checksum_type = "sha256" - file_checksum = "ace82f0d5e3586036cb72c5e46eb6222e93365503c51385496e3e2ee9999a5ad" + file_checksum = "62d025b9bc7ca0e1facfec74ae56ac13978b6745c58177f081d39fbb8041ed45" file_target_extension = "xz" file_unarchive_cmd = ["xz", "--decompress", "$ARCHIVE_PATH"] - file_urls = ["https://raspi.debian.net/tested/20231109_raspi_4_bookworm.img.xz"] + file_urls = ["https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2025-05-13/2025-05-13-raspios-bookworm-arm64-lite.img.xz"] image_build_method = "resize" image_chroot_env = ["PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin"] image_partitions { diff --git a/ci/arm64-rpi/arm64-rpi5.pkr.hcl b/ci/arm64-rpi/arm64-rpi5.pkr.hcl deleted file mode 100644 index dcdd912..0000000 --- a/ci/arm64-rpi/arm64-rpi5.pkr.hcl +++ /dev/null @@ -1,60 +0,0 @@ -variable "github_user" {} -variable "branch" {} - -source "arm" "joininbox-arm64-rpi5" { - file_checksum_type = "sha256" - file_checksum = "6ac3a10a1f144c7e9d1f8e568d75ca809288280a593eb6ca053e49b539f465a4" - file_target_extension = "xz" - file_unarchive_cmd = ["xz", "--decompress", "$ARCHIVE_PATH"] - file_urls = ["https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz"] - image_build_method = "resize" - image_chroot_env = ["PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin"] - image_partitions { - filesystem = "vfat" - mountpoint = "/boot" - name = "boot" - size = "256M" - start_sector = "8192" - type = "c" - } - image_partitions { - filesystem = "ext4" - mountpoint = "/" - name = "root" - size = "0" - start_sector = "532480" - type = "83" - } - image_path = "joininbox-arm64-rpi5.img" - image_size = "8G" - image_type = "dos" - qemu_binary_destination_path = "/usr/bin/qemu-aarch64-static" - qemu_binary_source_path = "/usr/bin/qemu-aarch64-static" -} - -build { - sources = ["source.arm.joininbox-arm64-rpi5"] - - provisioner "shell" { - inline = [ - "echo 'nameserver 1.1.1.1' >/etc/resolv.conf", - "echo 'nameserver 8.8.8.8' >>/etc/resolv.conf", - "echo $(hostname -I | awk '{print $1}') $(hostname) >>/etc/hosts", - "echo 127.0.1.1 $(hostname) >>/etc/hosts", - "echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections", - "apt-get update", - "apt-get install -y sudo wget", - "apt-get -y autoremove", - "apt-get -y clean", - ] - } - - provisioner "shell" { - environment_vars = [ - "github_user=${var.github_user}", - "branch=${var.branch}" - ] - script = "./joininbox.sh" - } - -} diff --git a/ci/arm64-rpi/arm64-rpi5.sh b/ci/arm64-rpi/arm64-rpi5.sh deleted file mode 100644 index 6f0a13b..0000000 --- a/ci/arm64-rpi/arm64-rpi5.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -e - -if [ $# -gt 0 ]; then - github_user=$1 -else - github_user=openoms -fi - -if [ $# -gt 1 ]; then - branch=$2 -else - branch=master -fi - -# Build the image in docker -echo -e "\nBuild Packer image..." -# from https://hub.docker.com/r/mkaczanowski/packer-builder-arm/tags -docker run --rm --privileged -v /dev:/dev -v ${PWD}:/build \ - mkaczanowski/packer-builder-arm:1.0.4@sha256:df09a8e249a292f10ca9b8cfd73420f5b987b6ac337d4ef28b6f4a8e61118822 \ - build -var "github_user=${github_user}" -var "branch=${branch}" arm64-rpi5.pkr.hcl From 56b06fafa4f58869745d61537ab3443db1eaeda6 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Fri, 6 Jun 2025 20:51:50 +0200 Subject: [PATCH 44/56] chore: adapt to new path to bitcoin.conf in raspiblitz (#171) --- FAQ.md | 2 +- prepare_remote_node.md | 8 ++++---- scripts/_functions.bitcoincore.sh | 16 ++++++++++++--- scripts/_functions.sh | 33 ++++++++++++++++++------------- scripts/info.importwallet.sh | 6 +++--- scripts/menu.config.sh | 6 +++++- scripts/menu.jam.sh | 6 +++++- 7 files changed, 50 insertions(+), 27 deletions(-) diff --git a/FAQ.md b/FAQ.md index 9d2169a..ae14865 100644 --- a/FAQ.md +++ b/FAQ.md @@ -447,7 +447,7 @@ You need to run rescanblockchain on that wallet in bitcoind after importing the Rescanning from the first SegWit block is sufficient for the default SegWit wallets. * Monitor progress (on a RaspiBlitz): -`sudo tail -fn 100 /mnt/hdd/bitcoin/debug.log` +`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`) ## 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** diff --git a/prepare_remote_node.md b/prepare_remote_node.md index 8c2fe41..628bc24 100644 --- a/prepare_remote_node.md +++ b/prepare_remote_node.md @@ -26,7 +26,7 @@ Since the RaspiBlitz v1.6 run this script: To set up manually: * Edit the bitcoin.conf: -`$ sudo nano /mnt/hdd/bitcoin/bitcoin.conf` +`$ sudo nano /mnt/hdd/app-data/bitcoin/bitcoin.conf` * Change the disablewallet option to 0: ``` @@ -40,7 +40,7 @@ In the terminal of the node - allow remote RPC connections to Bitcoin Core This can be skipped if you [connect through Tor](#tor-connection) 1) Edit the bitcoin.conf - `$ sudo nano /mnt/hdd/bitcoin/bitcoin.conf` + `$ sudo nano /mnt/hdd/app-data/bitcoin/bitcoin.conf` Add the values: * `rpcallowip=JOININBOX_IP` or `RANGE` @@ -109,8 +109,8 @@ Fill in the `Tor_Hidden_Service.onion` to the `rpc_host` in the `joinmarket.cfg` ### CONFIG -> CONNECT in JoininBox * Username: `raspibolt ` -* Password: `passwordB` or `sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcpassword | cut -c 13-` -* Host: `LAN_IP_OF_THE_NODE` or `sudo cat /mnt/hdd/tor/bitcoinrpc/hostname` +* Password: `passwordB` or `sudo cat /mnt/hdd/bitcoin/app-data/bitcoin.conf | grep rpcpassword | cut -c 13-` +* Host: `LAN_IP_OF_THE_NODE` or `sudo cat /mnt/hdd/app-data/tor/bitcoinrpc/hostname` * Port: `8332` --- diff --git a/scripts/_functions.bitcoincore.sh b/scripts/_functions.bitcoincore.sh index 013da91..ea03c49 100755 --- a/scripts/_functions.bitcoincore.sh +++ b/scripts/_functions.bitcoincore.sh @@ -334,7 +334,11 @@ function checkRPCwallet { if [ $runningEnv = standalone ]; then bitcoinConfPath="/home/bitcoin/.bitcoin/bitcoin.conf" elif [ $runningEnv = raspiblitz ]; then - bitcoinConfPath="/mnt/hdd/bitcoin/bitcoin.conf" + 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" @@ -433,8 +437,14 @@ function connectLocalNode() { fi rpc_wallet="wallet.dat" if [ $runningEnv = raspiblitz ]; then - rpc_user=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcuser | cut -c 9-) - rpc_pass=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcpassword | cut -c 13-) + if [ -f "/mnt/hdd/raspiblitz.conf" ]; then + rpc_user=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcuser | cut -c 9-) + rpc_pass=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcpassword | cut -c 13-) + else + rpc_user=$(sudo cat /mnt/hdd/app-data/bitcoin/bitcoin.conf | grep rpcuser | cut -c 9-) + rpc_pass=$(sudo cat /mnt/hdd/app-data/bitcoin/bitcoin.conf | grep rpcpassword | cut -c 13-) + fi + elif [ $runningEnv = mynode ]; then rpc_user=mynode rpc_pass=$(sudo cat /mnt/hdd/mynode/settings/.btcrpcpw) diff --git a/scripts/_functions.sh b/scripts/_functions.sh index 781ea2f..4c69bce 100755 --- a/scripts/_functions.sh +++ b/scripts/_functions.sh @@ -249,28 +249,33 @@ function generateJMconfig() { setJMconfigToSignet - elif [ -f "/mnt/hdd/bitcoin/bitcoin.conf" ]; then + elif [ $runningEnv = "raspiblitz" ]; then echo echo "## editing the joinmarket.cfg with the local bitcoin RPC settings." - - RPCUSER=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcuser | cut -c 9-) - sed -i "s/^rpc_user =.*/rpc_user = $RPCUSER/g" $JMcfgPath - echo "# rpc_user = $RPCUSER" - - PASSWORD_B=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcpassword | cut -c 13-) - sed -i "s/^rpc_password =.*/rpc_password = $PASSWORD_B/g" $JMcfgPath - echo "# rpc_password = $PASSWORD_B" - - RPCPORT=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep main.rpcport | cut -c 14-) - if [ ${#RPCPORT} -eq 0 ];then + if [ -f "/mnt/hdd/bitcoin/bitcoin.conf" ]; then + RPCUSER=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcuser | cut -c 9-) + sed -i "s/^rpc_user =.*/rpc_user = $RPCUSER/g" $JMcfgPath + echo "# rpc_user = $RPCUSER" + PASSWORD_B=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep rpcpassword | cut -c 13-) + sed -i "s/^rpc_password =.*/rpc_password = $PASSWORD_B/g" $JMcfgPath + echo "# rpc_password = $PASSWORD_B" + RPCPORT=$(sudo cat /mnt/hdd/bitcoin/bitcoin.conf | grep main.rpcport | cut -c 14-) + else + RPCUSER=$(sudo cat /mnt/hdd/app-data/bitcoin/bitcoin.conf | grep rpcuser | cut -c 9-) + sed -i "s/^rpc_user =.*/rpc_user = $RPCUSER/g" $JMcfgPath + echo "# rpc_user = $RPCUSER" + PASSWORD_B=$(sudo cat /mnt/hdd/app-data/bitcoin/bitcoin.conf | grep rpcpassword | cut -c 13-) + sed -i "s/^rpc_password =.*/rpc_password = $PASSWORD_B/g" $JMcfgPath + echo "# rpc_password = $PASSWORD_B" + RPCPORT=$(sudo cat /mnt/hdd/app-data/bitcoin/bitcoin.conf | grep main.rpcport | cut -c 14-) + fi + if [ ${#RPCPORT} -eq 0 ]; then RPCPORT=8332 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" - # set joinin.conf value /home/joinmarket/set.value.sh set network mainnet ${joininConfPath} fi diff --git a/scripts/info.importwallet.sh b/scripts/info.importwallet.sh index de803ca..d870ac1 100755 --- a/scripts/info.importwallet.sh +++ b/scripts/info.importwallet.sh @@ -36,7 +36,7 @@ Instructions to COPY wallets from another computer You can use the wallets from another JoinMarket instance " if [ "${CHOICE}" = "LAN" ]; then - if [ -f "/mnt/hdd/raspiblitz.conf" ] ; then + if [ -f "/mnt/hdd/raspiblitz.conf" ] || [ -f "/mnt/hdd/app-data/raspiblitz.conf" ] ; then echo "Both computers (the RaspiBlitz and the source computer with the wallet(s))" else echo "Both computers (the JoininBox and the source computer with the wallet(s))" @@ -62,9 +62,9 @@ elif [ "${CHOICE}" = "TOR" ]; then echo "torsocks scp ./*.jmdat joinmarket@${TOR_ADDRESS}:~/.joinmarket/wallets/" fi echo "" -if [ -f "/mnt/hdd/raspiblitz.conf" ] ; then +if [ -f "/mnt/hdd/raspiblitz.conf" ] || [ -f "/mnt/hdd/app-data/raspiblitz.conf" ] ; then echo "Use the PASSWORD_B to authorize the file transfer -(same as the rpcpassword in the /mnt/hdd/bitcoin/bitcoin.conf)." +(same as the rpcpassword in the bitcoin.conf)." else echo "This command will ask for the SSH PASSWORD of the JoininBox." fi diff --git a/scripts/menu.config.sh b/scripts/menu.config.sh index 093daea..9387221 100755 --- a/scripts/menu.config.sh +++ b/scripts/menu.config.sh @@ -10,7 +10,11 @@ if [ ${#network} -eq 0 ] || [ "${network}" = "unknown" ] ;then elif [ "${runningEnv}" = mynode ];then network=mainnet elif [ "${runningEnv}" = raspiblitz ];then - source /mnt/hdd/raspiblitz.conf + if [ -f "/mnt/hdd/raspiblitz.conf" ]; then + source /mnt/hdd/raspiblitz.conf + else + source /mnt/hdd/app-data/raspiblitz.conf + fi if [ $network = bitcoin ];then network=${chain}net else diff --git a/scripts/menu.jam.sh b/scripts/menu.jam.sh index 4034e34..7585874 100644 --- a/scripts/menu.jam.sh +++ b/scripts/menu.jam.sh @@ -1,7 +1,11 @@ #!/bin/bash source /home/joinmarket/joinin.conf -source /mnt/hdd/raspiblitz.conf +if [ -f "/mnt/hdd/raspiblitz.conf" ]; then + source /mnt/hdd/raspiblitz.conf +else + source /mnt/hdd/app-data/raspiblitz.conf +fi # BASIC MENU INFO HEIGHT=8 From a91cdc365791eab3e66591ab5558a2416df95eb4 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Fri, 6 Jun 2025 22:43:38 +0200 Subject: [PATCH 45/56] chore: remove the boltzmann tool (#172) --- scripts/menu.tools.sh | 24 +-------------- scripts/start.boltzmann.py | 62 -------------------------------------- 2 files changed, 1 insertion(+), 85 deletions(-) delete mode 100755 scripts/start.boltzmann.py diff --git a/scripts/menu.tools.sh b/scripts/menu.tools.sh index c159df8..b849fa4 100755 --- a/scripts/menu.tools.sh +++ b/scripts/menu.tools.sh @@ -26,17 +26,6 @@ function listCJcandidateTXNs { done } -function installBoltzmann { - if [ ! -f "/home/joinmarket/boltzmann/bvenv/bin/activate" ]; then - cd /home/joinmarket/ || exit 1 - git clone https://code.samourai.io/oxt/boltzmann.git - cd boltzmann || exit 1 - python3 -m venv bvenv - source bvenv/bin/activate || exit 1 - python setup.py install - fi -} - function dialog_inputbox { local title=$1 local text=$2 @@ -66,7 +55,7 @@ isLocalBitcoinCLI=$(sudo -u bitcoin bitcoin-cli -version | grep -c "Bitcoin Core isTxindex=$(sudo -u bitcoin cat /home/bitcoin/.bitcoin/bitcoin.conf | grep -c "txindex=1") # BASIC MENU INFO -HEIGHT=13 +HEIGHT=12 WIDTH=61 CHOICE_HEIGHT=7 TITLE="Tools" @@ -97,8 +86,6 @@ if [ "$isLocalBitcoinCLI" -gt 0 ] && [ "$isTxindex" -gt 0 ]; then CHOICE_HEIGHT=$((CHOICE_HEIGHT + 1)) fi fi -OPTIONS+=( - BOLTZMANN "Analyze the entropy of a transaction") if [ "${runningEnv}" != mynode ]; then OPTIONS+=( PASSWORD "Change the ssh password") @@ -186,15 +173,6 @@ CHECKTXN) echo "Press ENTER to return to the menu..." read key ;; -BOLTZMANN) - dialog_inputbox "Boltzmann transaction entropy analysis" "\nUsing: https://code.samourai.io/oxt/boltzmann\n\nPaste a TXID to analyze" 11 71 - clear - installBoltzmann - python /home/joinmarket/start.boltzmann.py --txid=$dialog_output - echo - echo "Press ENTER to return to the menu..." - read key - ;; CJFINDER) BLOCKHEIGHT=$(customRPC "" "getblockchaininfo" "" | grep blocks | awk '{print $2}' | cut -d, -f1) diff --git a/scripts/start.boltzmann.py b/scripts/start.boltzmann.py deleted file mode 100755 index 100fcc5..0000000 --- a/scripts/start.boltzmann.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 - -import configparser -import os -import sys -import getopt - -def main(argv): - - description = "\nBoltzmann - https://code.samourai.io/oxt/boltzmann/\n\ -A python script computing the entropy of Bitcoin transactions and the linkability of their inputs and outputs.\n\ -For a description of the metrics and related discussions:\n\ -Bitcoin Transactions & Privacy (part 1) : https://gist.github.com/LaurentMT/e758767ca4038ac40aaf\n\ -Bitcoin Transactions & Privacy (part 2) : https://gist.github.com/LaurentMT/d361bca6dc52868573a2\n\ -Bitcoin Transactions & Privacy (part 3) : https://gist.github.com/LaurentMT/e8644d5bc903f02613c6\n\ -\n\ -WARNING: this feature is highly experimental and not optimised to be used with JoinMarket." - os.system('clear') - print(description) - - txids = '' - try: - opts, args = getopt.getopt(argv,"t:",["txids="]) - except getopt.GetoptError: - print('python run.boltzmann.py --txids=TXID1,TXID2') - sys.exit(2) - - for opt, arg in opts: - if opt in ('-t', '--txids'): - txids = arg - config = configparser.ConfigParser(strict=False) - - config.read('/home/joinmarket/.joinmarket/joinmarket.cfg') - - rpc_user = config['BLOCKCHAIN']['rpc_user'] - rpc_password = config['BLOCKCHAIN']['rpc_password'] - rpc_host = config['BLOCKCHAIN']['rpc_host'] - rpc_port = config['BLOCKCHAIN']['rpc_port'] - - tor = '' - if rpc_host.find('.onion') >= 0: - print('# Connecting to bitcoind RPC over Tor') - tor = 'torsocks' - - boltzmannpath = "/home/joinmarket/boltzmann/" - - bvenv = ". "+boltzmannpath+"bvenv/bin/activate" - - boltzmann = tor+" python "+boltzmannpath+"boltzmann/ludwig.py -p -x 30 \ ---txids="+txids - - run = bvenv+";"+"export BOLTZMANN_RPC_USERNAME="+rpc_user+";\ - export BOLTZMANN_RPC_PASSWORD="+rpc_password+";\ - export BOLTZMANN_RPC_HOST="+rpc_host+";\ - export BOLTZMANN_RPC_PORT="+rpc_port+";"+boltzmann+" 2>/dev/null" - - print('# Exporting RPC connection details') - print('# Running the command:\n'+boltzmann+'\n') - os.system(run) - -if __name__ == "__main__": - main(sys.argv[1:]) From 1b9eca24478ec92e112c9ab9e05b29e5d4866cb4 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Fri, 6 Jun 2025 23:05:36 +0200 Subject: [PATCH 46/56] chore: add release-tag to makefile (#173) --- Makefile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Makefile b/Makefile index f26dc65..35b4f7e 100644 --- a/Makefile +++ b/Makefile @@ -35,3 +35,12 @@ arm64-rpi-image: # Compute checksum of the compressed image cd ci/arm64-rpi && \ sha256sum joininbox-arm64-rpi.img.gz > joininbox-arm64-rpi.img.gz.sha256 + +release-tag: + @if [ -z "$(TAG)" ]; then \ + echo "Error: TAG parameter is required. Usage: make release-tag TAG=v0.7.4"; \ + exit 1; \ + fi + @echo "Creating signed tag $(TAG) and pushing to origin..." + git tag -s $(TAG) -m "$(TAG)" && git push origin $(TAG) + @echo "Successfully created and pushed signed tag $(TAG)" From e69d6006a4ed039cf7398cae4c745e8dd40db9b3 Mon Sep 17 00:00:00 2001 From: openoms <43343391+openoms@users.noreply.github.com> Date: Fri, 6 Jun 2025 23:10:54 +0200 Subject: [PATCH 47/56] feat: add shellcheck test to github actions (#174) * feat: add shellcheck test to github actions * fix: shellcheck errors --- .github/workflows/test-shellcheck.yml | 19 +++++++++++++++++++ scripts/set.value.sh | 2 +- scripts/standalone/menu.specter.sh | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test-shellcheck.yml diff --git a/.github/workflows/test-shellcheck.yml b/.github/workflows/test-shellcheck.yml new file mode 100644 index 0000000..f1c7b6f --- /dev/null +++ b/.github/workflows/test-shellcheck.yml @@ -0,0 +1,19 @@ +name: "Test Shellcheck" + +on: + workflow_dispatch: + push: + branches: ["master"] + pull_request: + branches: ["master"] + +jobs: + shellcheck: + name: Run Shellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + with: + severity: error diff --git a/scripts/set.value.sh b/scripts/set.value.sh index 6b5ee39..4c28aaf 100644 --- a/scripts/set.value.sh +++ b/scripts/set.value.sh @@ -30,7 +30,7 @@ if [ "$1" = "set" ]; then # check that config file exists raspiblitzConfExists=$(ls ${configFile} 2>/dev/null | grep -c "${configFile}") if [ ${raspiblitzConfExists} -eq 0 ]; then - echo "# blitz.conf.sh $@" + echo "# blitz.conf.sh $*" echo "# FAIL: missing config file: ${configFile}" exit 3 fi diff --git a/scripts/standalone/menu.specter.sh b/scripts/standalone/menu.specter.sh index 3e6324f..74abe9c 100755 --- a/scripts/standalone/menu.specter.sh +++ b/scripts/standalone/menu.specter.sh @@ -1,3 +1,5 @@ +#!/bin/bash + source /home/joinmarket/_functions.sh # add default value to config if needed 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 48/56] 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 49/56] 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 50/56] 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 51/56] 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 52/56] 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 53/56] 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 54/56] 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 55/56] 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 56/56] 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