From 0849d60f284d0428802bc3914503e98cf77f852b Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Mon, 8 Dec 2025 01:58:19 -0500 Subject: [PATCH] Run Bitcoin, Monero nodes on Alpine While prior this didn't work well, presumably due to stack size limitations, a shell script is included to raise the default stack size limit. This should be tried again. --- orchestration/increase_default_stack_size.sh | 166 +++++++++++++++++++ orchestration/src/networks/bitcoin.rs | 4 +- orchestration/src/networks/monero.rs | 16 +- 3 files changed, 181 insertions(+), 5 deletions(-) create mode 100755 orchestration/increase_default_stack_size.sh diff --git a/orchestration/increase_default_stack_size.sh b/orchestration/increase_default_stack_size.sh new file mode 100755 index 00000000..accbdb1c --- /dev/null +++ b/orchestration/increase_default_stack_size.sh @@ -0,0 +1,166 @@ +# Raises `PT_GNU_STACK`'s memory to be at least 8 MB. +# +# This causes `musl` to use a 8 MB default for new threads, resolving the primary +# compatibility issue faced when executing a program on a `musl` system. +# +# See https://wiki.musl-libc.org/functional-differences-from-glibc.html#Thread-stack-size +# for reference. This differs that instead of setting at time of link, it +# patches the binary as an already-linked ELF executable. + +#!/bin/bash +set -eo pipefail + +ELF="$1" +if [ ! -f "$ELF" ]; then + echo "\`increase_default_stack_size.sh\` [ELF binary]" + echo "" + echo "Sets the \`PT_GNU_STACK\` program header to its existing value or 8 MB," + echo "whichever is greater." + exit 1 +fi + +function hex { + hexdump -e '1 1 "%.2x"' -v +} +function read_bytes { + dd status=none bs=1 skip=$1 count=$2 if="$ELF" | hex +} +function write_bytes { + POS=$1 + BYTES=$2 + while [ ! $BYTES = "" ]; do + printf "\x$(printf $BYTES | head -c2)" | dd status=none conv=notrunc bs=1 seek=$POS of="$ELF" + # Start with the third byte, as in, after the first two bytes + BYTES=$(printf $BYTES | tail -c+3) + POS=$(($POS + 1)) + done +} + +# Magic +MAGIC=$(read_bytes 0 4) +if [ ! $MAGIC = $(printf "\x7fELF" | hex) ]; then + echo "Not ELF" + exit 2 +fi + +# 1 if 32-bit, 2 if 64-bit +BITS=$(read_bytes 4 1) +case $BITS in + "01") BITS=32;; + "02") BITS=64;; + *) + echo "Not 32- or 64- bit" + exit 3 + ;; +esac + +# For `value_per_bits a b`, `a` if 32-bit and `b` if 64-bit +function value_per_bits { + RESULT=$(($1)) + if [ $BITS = 64 ]; then + RESULT=$(($2)) + fi + printf $RESULT +} + +# Read an integer by its offset, differing depending on if 32- or 64-bit +function read_integer_by_offset { + OFFSET=$(value_per_bits $1 $2) + printf $(( 0x$(swap_native_endian $(read_bytes $OFFSET $3)) )) +} + +# 1 if little-endian, 2 if big-endian +LITTLE_ENDIAN=$(read_bytes 5 1) +case $LITTLE_ENDIAN in + "01") LITTLE_ENDIAN=1;; + "02") LITTLE_ENDIAN=0;; + *) + echo "Not little- or big- endian" + exit 4 + ;; +esac + +# While this script is written in big-endian, we need to work with the file in +# its declared endian. This function swaps from big to native, or vice versa, +# as necessary. +function swap_native_endian { + BYTES="$1" + if [ "$BYTES" = "" ]; then + read BYTES + fi + + if [ $LITTLE_ENDIAN -eq 0 ]; then + printf $BYTES + return + fi + + while [ ! $BYTES = "" ]; do + printf $(printf $BYTES | tail -c2) + BYTES=$(printf $BYTES | head -c-2) + done +} + +ELF_VERSION=$(read_bytes 6 1) +if [ ! $ELF_VERSION = "01" ]; then + echo "Unknown ELF Version ($ELF_VERSION)" + exit 5 +fi + +ELF_VERSION_2=$(read_bytes $((0x14)) 4) +if [ ! $ELF_VERSION_2 = $(swap_native_endian 00000001) ]; then + echo "Unknown secondary ELF Version ($ELF_VERSION_2)" + exit 6 +fi + +# Find where the program headers are +PROGRAM_HEADERS_OFFSET=$(read_integer_by_offset 0x1c 0x20 $(value_per_bits 4 8)) +PROGRAM_HEADER_SIZE=$(value_per_bits 0x20 0x38) +DECLARED_PROGRAM_HEADER_SIZE=$(read_integer_by_offset 0x2a 0x36 2) +if [ ! $PROGRAM_HEADER_SIZE -eq $DECLARED_PROGRAM_HEADER_SIZE ]; then + echo "Unexpected size of a program header ($DECLARED_PROGRAM_HEADER_SIZE)" + exit 7 +fi +function program_header_start { + printf $(($PROGRAM_HEADERS_OFFSET + ($1 * $PROGRAM_HEADER_SIZE))) +} +function read_program_header { + read_bytes $(program_header_start $1) $PROGRAM_HEADER_SIZE +} + +# Iterate over each program header +PROGRAM_HEADERS=$(read_integer_by_offset 0x2c 0x38 2) +NEXT_PROGRAM_HEADER=$(( $PROGRAM_HEADERS - 1 )) +FOUND=0 +while [ $NEXT_PROGRAM_HEADER -ne -1 ]; do + THIS_PROGRAM_HEADER=$NEXT_PROGRAM_HEADER + NEXT_PROGRAM_HEADER=$(( $NEXT_PROGRAM_HEADER - 1 )) + PROGRAM_HEADER=$(read_program_header $THIS_PROGRAM_HEADER) + + HEADER_TYPE=$(printf $PROGRAM_HEADER | head -c8) + # `PT_GNU_STACK` + # https://github.com/torvalds/linux/blob/c2f2b01b74be8b40a2173372bcd770723f87e7b2/include/uapi/linux/elf.h#L41 + if [ ! "$(swap_native_endian $HEADER_TYPE)" = "6474e551" ]; then + continue + fi + FOUND=1 + + MEMSZ_OFFSET=$(( $(program_header_start $THIS_PROGRAM_HEADER) + $(value_per_bits 0x14 0x28) )) + MEMSZ_LEN=$(value_per_bits 4 8) + # `MEMSZ_OFFSET MEMSZ_OFFSET` as we've already derived it depending on the amount of bits + MEMSZ=$(read_integer_by_offset $MEMSZ_OFFSET $MEMSZ_OFFSET $MEMSZ_LEN) + DESIRED_STACK_SIZE=$((8 * 1024 * 1024)) + # Only run if the inherent value is _smaller_ + if [ $MEMSZ -lt $DESIRED_STACK_SIZE ]; then + # `2 *`, as this is its length in hexadecimal + HEX_MEMSZ=$(printf %."$((2 * $MEMSZ_LEN))"x $DESIRED_STACK_SIZE) + write_bytes $MEMSZ_OFFSET $(swap_native_endian $HEX_MEMSZ) + fi +done + +if [ $FOUND -eq 0 ]; then + echo "\`PT_GNU_STACK\` program header not found" + exit 8 +fi + +echo "All instances of \`PT_GNU_STACK\` patched to be at least 8 MB" +exit 0 diff --git a/orchestration/src/networks/bitcoin.rs b/orchestration/src/networks/bitcoin.rs index c98f383c..b1ff3735 100644 --- a/orchestration/src/networks/bitcoin.rs +++ b/orchestration/src/networks/bitcoin.rs @@ -29,7 +29,7 @@ RUN tar xzvf bitcoin-${BITCOIN_VERSION}-$(uname -m)-linux-gnu.tar.gz RUN mv bitcoin-${BITCOIN_VERSION}/bin/bitcoind . "#; - let setup = mimalloc(Os::Debian) + DOWNLOAD_BITCOIN; + let setup = mimalloc(Os::Alpine) + DOWNLOAD_BITCOIN; let run_bitcoin = format!( r#" @@ -43,7 +43,7 @@ CMD ["/run.sh"] network.label() ); - let run = os(Os::Debian, "", "bitcoin") + &run_bitcoin; + let run = os(Os::Alpine, "", "bitcoin") + &run_bitcoin; let res = setup + &run; let mut bitcoin_path = orchestration_path.to_path_buf(); diff --git a/orchestration/src/networks/monero.rs b/orchestration/src/networks/monero.rs index e5005aae..5847092c 100644 --- a/orchestration/src/networks/monero.rs +++ b/orchestration/src/networks/monero.rs @@ -21,7 +21,7 @@ fn monero_internal( }; #[rustfmt::skip] - let download_monero = format!(r#" + let mut download_monero = format!(r#" FROM alpine:latest AS monero RUN apk --no-cache add wget gnupg @@ -41,6 +41,16 @@ RUN tar -xvjf monero-linux-{arch}-v{MONERO_VERSION}.tar.bz2 --strip-components=1 network.label(), ); + if os == Os::Alpine { + // Increase the default stack size, as Monero does heavily use its stack + download_monero += &format!( + r#" +ADD orchestration/increase_default_stack_size.sh . +RUN ./increase_default_stack_size.sh {monero_binary} +"# + ); + } + let setup = mimalloc(os) + &download_monero; let run_monero = format!( @@ -69,13 +79,13 @@ CMD ["/run.sh"] } pub fn monero(orchestration_path: &Path, network: Network) { - monero_internal(network, Os::Debian, orchestration_path, "monero", "monerod", "18080 18081") + monero_internal(network, Os::Alpine, orchestration_path, "monero", "monerod", "18080 18081") } pub fn monero_wallet_rpc(orchestration_path: &Path) { monero_internal( Network::Dev, - Os::Debian, + Os::Alpine, orchestration_path, "monero-wallet-rpc", "monero-wallet-rpc",