diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4578d1c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +target +dist diff --git a/.gitignore b/.gitignore index ad67955..9aec134 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # will have compiled files and executables debug target +dist # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..093e654 --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +SHELL := /bin/sh + +CONTAINER_ENGINE ?= $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null) +ifeq ($(origin PACKAGE_VERSION),undefined) +PACKAGE_VERSION := $(shell ./scripts/package-version) +endif +DEB_OUTPUT_DIR ?= dist +BUILDER_IMAGE ?= monad-services-builder:local + +.PHONY: build builder deb deb-host clean + +build: + ./scripts/build-binaries + +# Needs the docker/builder toolchain on the host; `make deb` needs only a container engine. +deb-host: + PACKAGE_VERSION=$(PACKAGE_VERSION) DEB_OUTPUT_DIR=$(DEB_OUTPUT_DIR) ./scripts/build-deb + +builder: + @test -n "$(CONTAINER_ENGINE)" || { echo "Install Docker or Podman, or set CONTAINER_ENGINE."; exit 1; } + $(CONTAINER_ENGINE) build -t $(BUILDER_IMAGE) -f docker/builder/Dockerfile . + +deb: builder + @mkdir -p $(DEB_OUTPUT_DIR); iidfile=$$(mktemp); \ + trap 'rm -f "$$iidfile"' EXIT; \ + $(CONTAINER_ENGINE) build \ + --iidfile "$$iidfile" \ + --build-arg BUILDER_IMAGE=$(BUILDER_IMAGE) \ + --build-arg PACKAGE_VERSION=$(PACKAGE_VERSION) \ + -f docker/debian-package/Dockerfile . && \ + container_id=$$($(CONTAINER_ENGINE) create "$$(cat "$$iidfile")" true) && \ + trap '$(CONTAINER_ENGINE) rm -f $$container_id >/dev/null; rm -f "$$iidfile"' EXIT && \ + $(CONTAINER_ENGINE) cp "$$container_id":/out/. $(DEB_OUTPUT_DIR) + +clean: + rm -rf $(DEB_OUTPUT_DIR) diff --git a/debian/DEBIAN/conffiles b/debian/DEBIAN/conffiles new file mode 100644 index 0000000..06d5bf0 --- /dev/null +++ b/debian/DEBIAN/conffiles @@ -0,0 +1 @@ +/etc/monad-services/env diff --git a/debian/DEBIAN/control.in b/debian/DEBIAN/control.in new file mode 100644 index 0000000..7094365 --- /dev/null +++ b/debian/DEBIAN/control.in @@ -0,0 +1,10 @@ +Package: monad-services +Version: @PACKAGE_VERSION@ +Section: net +Priority: optional +Architecture: amd64 +Maintainer: Category Labs +Depends: @DEPENDS@, ca-certificates +Description: Monad archive and indexing services + Archive, validation, indexing, and block-writing binaries maintained by the + monad-services repository. diff --git a/debian/DEBIAN/postinst b/debian/DEBIAN/postinst new file mode 100755 index 0000000..d2c4735 --- /dev/null +++ b/debian/DEBIAN/postinst @@ -0,0 +1,10 @@ +#!/bin/sh +set -eu + +case "$1" in + configure) + if [ -d /run/systemd/system ]; then + systemctl daemon-reload || true + fi + ;; +esac diff --git a/debian/DEBIAN/postrm b/debian/DEBIAN/postrm new file mode 100755 index 0000000..8417f25 --- /dev/null +++ b/debian/DEBIAN/postrm @@ -0,0 +1,13 @@ +#!/bin/sh +set -eu + +case "$1" in + remove|purge) + if [ -d /run/systemd/system ]; then + systemctl daemon-reload || true + for unit in monad-services-archiver monad-services-archive-checker monad-services-indexer monad-services-block-writer; do + systemctl reset-failed "$unit.service" >/dev/null 2>&1 || true + done + fi + ;; +esac diff --git a/debian/DEBIAN/prerm b/debian/DEBIAN/prerm new file mode 100755 index 0000000..24c7c94 --- /dev/null +++ b/debian/DEBIAN/prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +case "$1" in + remove|deconfigure) + if [ -d /run/systemd/system ]; then + for unit in monad-services-archiver monad-services-archive-checker monad-services-indexer monad-services-block-writer; do + systemctl stop "$unit.service" || true + done + fi + ;; +esac diff --git a/debian/etc/monad-services/env b/debian/etc/monad-services/env new file mode 100644 index 0000000..506d8f8 --- /dev/null +++ b/debian/etc/monad-services/env @@ -0,0 +1,3 @@ +# Configuration for the monad-services units. Variables set here are +# expanded into each unit's ExecStart line; a unit fails to start when a +# variable it names is missing. diff --git a/debian/usr/lib/systemd/system/monad-services-archive-checker.service b/debian/usr/lib/systemd/system/monad-services-archive-checker.service new file mode 100644 index 0000000..a329031 --- /dev/null +++ b/debian/usr/lib/systemd/system/monad-services-archive-checker.service @@ -0,0 +1,17 @@ +[Unit] +Description=Monad Archive Checker (monad-services) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=monad +Group=monad +EnvironmentFile=-/etc/monad-services/env +ExecStart=/usr/lib/monad-services/monad-archive-checker +Restart=always +RestartSec=5 +LimitNOFILE=1048576 + +[Install] +WantedBy=multi-user.target diff --git a/debian/usr/lib/systemd/system/monad-services-archiver.service b/debian/usr/lib/systemd/system/monad-services-archiver.service new file mode 100644 index 0000000..701fa8a --- /dev/null +++ b/debian/usr/lib/systemd/system/monad-services-archiver.service @@ -0,0 +1,17 @@ +[Unit] +Description=Monad Archiver (monad-services) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=monad +Group=monad +EnvironmentFile=-/etc/monad-services/env +ExecStart=/usr/lib/monad-services/monad-archiver +Restart=always +RestartSec=5 +LimitNOFILE=1048576 + +[Install] +WantedBy=multi-user.target diff --git a/debian/usr/lib/systemd/system/monad-services-block-writer.service b/debian/usr/lib/systemd/system/monad-services-block-writer.service new file mode 100644 index 0000000..514bee4 --- /dev/null +++ b/debian/usr/lib/systemd/system/monad-services-block-writer.service @@ -0,0 +1,17 @@ +[Unit] +Description=Monad Block Writer (monad-services) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=monad +Group=monad +EnvironmentFile=-/etc/monad-services/env +ExecStart=/usr/lib/monad-services/monad-block-writer +Restart=always +RestartSec=5 +LimitNOFILE=1048576 + +[Install] +WantedBy=multi-user.target diff --git a/debian/usr/lib/systemd/system/monad-services-indexer.service b/debian/usr/lib/systemd/system/monad-services-indexer.service new file mode 100644 index 0000000..4896c47 --- /dev/null +++ b/debian/usr/lib/systemd/system/monad-services-indexer.service @@ -0,0 +1,17 @@ +[Unit] +Description=Monad Indexer (monad-services) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=monad +Group=monad +EnvironmentFile=-/etc/monad-services/env +ExecStart=/usr/lib/monad-services/monad-indexer +Restart=always +RestartSec=5 +LimitNOFILE=1048576 + +[Install] +WantedBy=multi-user.target diff --git a/debian/usr/share/doc/monad-services/copyright b/debian/usr/share/doc/monad-services/copyright new file mode 100644 index 0000000..bfbd674 --- /dev/null +++ b/debian/usr/share/doc/monad-services/copyright @@ -0,0 +1,9 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: monad-services +Source: https://github.com/category-labs/monad-services + +Files: * +Copyright: 2026 Category Labs, Inc. +License: GPL-3 + On Debian systems, the complete text of the GNU General Public + License version 3 can be found in /usr/share/common-licenses/GPL-3. diff --git a/docker/builder/Dockerfile b/docker/builder/Dockerfile new file mode 100644 index 0000000..0a8b0bf --- /dev/null +++ b/docker/builder/Dockerfile @@ -0,0 +1,30 @@ +FROM ubuntu:26.04 + +ENV DEBIAN_FRONTEND=noninteractive \ + RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:${PATH} \ + CC=gcc-15 \ + CXX=g++-15 \ + LIBCLANG_PATH=/usr/lib/llvm-19/lib \ + CMAKE_GENERATOR=Ninja \ + TRIEDB_TARGET=triedb_driver \ + ASMFLAGS=-march=haswell \ + CFLAGS=-march=haswell \ + CXXFLAGS=-march=haswell + +RUN apt-get update && apt-get install -y --no-install-recommends \ + binutils ca-certificates clang-19 cmake curl g++-15 gcc-15 git \ + libabsl-dev libarchive-dev libbenchmark-dev libboost-all-dev libbrotli-dev libcap-dev \ + libcgroup-dev libclang-19-dev libcli11-dev libcrypto++-dev libgmp-dev \ + libgmock-dev libgtest-dev libhugetlbfs-dev libmagicenum-dev libmimalloc-dev \ + libsecp256k1-dev libssl-dev \ + libstdc++-15-dev libtbb-dev liburing-dev libzstd-dev ninja-build pkg-config \ + && rm -rf /var/lib/apt/lists/* + +# The toolchain version comes from rust-toolchain.toml, which the rustup +# shims on PATH also consult at cargo time, so image and build can't drift. +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain none +COPY rust-toolchain.toml /tmp/rust-toolchain/rust-toolchain.toml +RUN cd /tmp/rust-toolchain && rustup toolchain install && rm -rf /tmp/rust-toolchain diff --git a/docker/debian-package/Dockerfile b/docker/debian-package/Dockerfile new file mode 100644 index 0000000..c738119 --- /dev/null +++ b/docker/debian-package/Dockerfile @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1 + +# Toolchain comes from docker/builder/Dockerfile. `make deb` builds it +# locally; override to reuse a prebuilt copy. +ARG BUILDER_IMAGE=monad-services-builder:local +FROM ${BUILDER_IMAGE} AS package + +ARG PACKAGE_VERSION +ENV DEBIAN_FRONTEND=noninteractive \ + CARGO_NET_GIT_FETCH_WITH_CLI=true + +# Only what docker/builder does not already carry: build-essential for the +# packaging tools (including dpkg-shlibdeps) and patchelf for runpaths. +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential patchelf \ + && rm -rf /var/lib/apt/lists/* + +# .dockerignore strips .git, so scripts/package-version cannot run here. +RUN test -n "$PACKAGE_VERSION" + +WORKDIR /src +COPY . . + +RUN --mount=type=cache,id=monad-services-cargo-registry,target=/usr/local/cargo/registry \ + --mount=type=cache,id=monad-services-cargo-git,target=/usr/local/cargo/git \ + --mount=type=cache,id=monad-services-target,target=/src/target \ + PACKAGE_VERSION="${PACKAGE_VERSION}" DEB_OUTPUT_DIR=/out ./scripts/build-deb + +FROM scratch +COPY --from=package /out /out diff --git a/scripts/build-binaries b/scripts/build-binaries new file mode 100755 index 0000000..aaaeddb --- /dev/null +++ b/scripts/build-binaries @@ -0,0 +1,25 @@ +#!/usr/bin/env sh +set -eu + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) + +# --- toolchain (matches the release build, in docker/debian-package/Dockerfile) --- +export CC="${CC:-gcc-15}" +export CXX="${CXX:-g++-15}" +export CMAKE_GENERATOR="${CMAKE_GENERATOR:-Ninja}" +# monad-triedb's build script builds libtriedb_driver.so from this target. +export TRIEDB_TARGET="${TRIEDB_TARGET:-triedb_driver}" +# Arch flags reach the cargo build only; the cmake toolchain file owns its own. +asmflags="${ASMFLAGS:--march=haswell}" +cflags="${CFLAGS:--march=haswell}" +cxxflags="${CXXFLAGS:--march=haswell}" + +cd "$root_dir" + +# --- rust binaries --- +# monad-triedb and the execution bindings it needs are built by Cargo build +# scripts out of the pinned monad-bft git dependency; there is no local +# execution checkout to build separately. +ASMFLAGS="$asmflags" CFLAGS="$cflags" \ +CXXFLAGS="$cxxflags -DQUILL_ACTIVE_LOG_LEVEL=QUILL_LOG_LEVEL_CRITICAL" \ +cargo build --locked --release --package monad-archive --bins diff --git a/scripts/build-deb b/scripts/build-deb new file mode 100755 index 0000000..b4d3573 --- /dev/null +++ b/scripts/build-deb @@ -0,0 +1,129 @@ +#!/usr/bin/env sh +set -eu + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +output_dir=${DEB_OUTPUT_DIR:-"$root_dir/dist"} +strip_symbols=${STRIP_SYMBOLS:-1} +# Stage outside target/, which is a shared cache mount in container builds. +stage_root=$(mktemp -d) +trap 'rm -rf "$stage_root"' EXIT +stage_dir="$stage_root/debian/monad-services" + +# --- version --- +version=${PACKAGE_VERSION:-$("$root_dir/scripts/package-version")} +case "$version" in + *[!0-9A-Za-z.+:~\-]*) echo "Invalid Debian version: $version" >&2; exit 2 ;; +esac + +cd "$root_dir" + +# --- build --- +"$root_dir/scripts/build-binaries" + +# --- stage binaries --- +# Everything installs into the package-private /usr/lib/monad-services: +# Debian policy (FHS, section 9.1) forbids packages from touching +# /usr/local, and the monad package ships the same binary names and +# library copies under /usr/local, so the private directory is also what +# lets the two packages be co-installed. The binaries find the libraries +# through RUNPATH, set below, instead of ldconfig. +lib_dir="$stage_dir/usr/lib/monad-services" +mkdir -p "$stage_dir/DEBIAN" "$lib_dir" + +for binary in monad-archiver monad-archive-checker monad-indexer monad-block-writer; do + install -m 0755 "target/release/$binary" "$lib_dir/$binary" +done + +# --- stage libraries --- +# Bundle libtriedb_driver.so, which is loaded with dlopen at runtime and so +# never appears as a declared dependency, plus every library that the staged +# binaries or the driver resolve from inside this source tree. Those are +# build products the target host does not have. ldd lists dependencies +# transitively, so one pass is enough, and dpkg-shlibdeps below fails the +# build if anything is missed here. +triedb=$(find target/release -name 'libtriedb_driver.so' -exec ls -Lt {} + | head -n 1) +test -n "$triedb" || { echo "libtriedb_driver.so not found under target/release" >&2; exit 2; } +cp "$triedb" "$lib_dir/" +libs=$(ldd "$lib_dir"/* \ + | awk -v root="$root_dir/" '$2 == "=>" && index($3, root) == 1 { print $3 }' \ + | sort -u) +seen=" " +for lib in $libs; do + base=${lib##*/} + case $seen in + *" $base "*) + echo "Bundled library basename collision: $base ($lib)" >&2 + exit 2 + ;; + esac + seen="$seen$base " + cp "$lib" "$lib_dir/" +done + +# The 26.04 builder's libsecp256k1 soname exists on no fleet host; bundle it +# like Jenkins. A copy staged from the build tree above takes precedence. +secp=/usr/lib/x86_64-linux-gnu/libsecp256k1.so.6 +if [ -e "$secp" ] && [ ! -e "$lib_dir/${secp##*/}" ]; then + cp -L "$secp" "$lib_dir/" +fi + +if [ "$strip_symbols" = 1 ]; then + find "$lib_dir" -type f ! -name '*.so*' -exec strip --strip-all {} \; + find "$lib_dir" -type f -name '*.so*' -exec strip --strip-unneeded {} \; +fi + +# --- check linkage & compute library dependencies --- +# Strip build-tree runpaths, then run dpkg-shlibdeps to generate Depends. +# +# For every library our binaries need, dpkg-shlibdeps locates the file and +# maps it to a package: our own libraries (such as libtriedb_driver.so) +# resolve inside the debian/monad-services staging tree and need no Depends +# entry; system libraries become versioned Depends. +# +# Runpaths into the build tree have to go first, or a library that's absent +# on the target host still resolves here and the check passes wrongly. +find "$lib_dir" -type f \ + -exec patchelf --remove-rpath {} \; + +# dpkg-shlibdeps only warns about libraries it cannot find, so fail on them here. +unresolved=$(LD_LIBRARY_PATH="$lib_dir" \ + ldd "$lib_dir"/* 2>/dev/null \ + | grep 'not found' || true) +if [ -n "$unresolved" ]; then + printf 'Unresolved shared libraries after staging:\n%s\n' "$unresolved" >&2 + exit 2 +fi + +printf 'Source: monad-services\n\nPackage: monad-services\nArchitecture: amd64\nDescription: stub\n' \ + > "$stage_root/debian/control" +lib_deps=$(cd "$stage_root" && dpkg-shlibdeps -O \ + -ldebian/monad-services/usr/lib/monad-services \ + debian/monad-services/usr/lib/monad-services/*) +lib_deps=${lib_deps#shlibs:Depends=} + +# --- set runpaths --- +# The private directory is outside the ldconfig search path, so everything +# carries a $ORIGIN RUNPATH: the binaries sit next to the libraries, dlopen +# of libtriedb_driver.so honours the calling executable's RUNPATH, and the +# libraries reference each other the same way. This keeps the monad +# package's copies of the same libraries from ever resolving into these +# services, or vice versa. +find "$lib_dir" -type f \ + -exec patchelf --set-rpath '$ORIGIN' {} \; + +# --- debian metadata --- +cp -a debian/. "$stage_dir/" +sed -e "s/@PACKAGE_VERSION@/$version/" -e "s%@DEPENDS@%$lib_deps%" \ + debian/DEBIAN/control.in > "$stage_dir/DEBIAN/control" +rm "$stage_dir/DEBIAN/control.in" + +# Policy 12.7 wants a compressed Debian changelog next to the static +# copyright file; the git history is the real changelog, so ship a stub +# entry that points at it. +printf 'monad-services (%s) unstable; urgency=medium\n\n * See https://github.com/category-labs/monad-services for the change history.\n\n -- Category Labs %s\n' \ + "$version" "$(date -R)" \ + | gzip -9n > "$stage_dir/usr/share/doc/monad-services/changelog.Debian.gz" + +# --- package --- +mkdir -p "$output_dir" +dpkg-deb --root-owner-group --build "$stage_dir" "$output_dir/monad-services_${version}_amd64.deb" diff --git a/scripts/package-version b/scripts/package-version new file mode 100755 index 0000000..24b9083 --- /dev/null +++ b/scripts/package-version @@ -0,0 +1,10 @@ +#!/usr/bin/env sh +# Single source of truth for the Debian package version, used by the Makefile +# and as scripts/build-deb's fallback. Requires a git checkout; container +# builds have no .git and must receive PACKAGE_VERSION explicitly. +set -eu + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +timestamp=$(git -C "$root_dir" log -1 --format=%cd --date=format:%Y%m%d-%H%M) +sha=$(git -C "$root_dir" rev-parse --short=7 HEAD) +printf '0.0-%s-%s\n' "$timestamp" "$sha"