Packaging satd
This document is the authoritative reference for downstream packagers (Umbrel, Start9, RaspiBlitz, MyNode, BTCPay, Debian/Fedora/Alpine, Homebrew, Nix). It describes file layout, signals, ports, config surface, runtime model, and the contract satd offers a packager.
The user-facing operator surfaces are documented elsewhere in this manual —
see Observability & Metrics and Configuration, Tuning &
Reload. The deviation catalog vs. Bitcoin Core is in
CORE_DIFFERENCES.md.
The not-yet-shipped ecosystem/packaging direction is tracked in
ROADMAP.md.
Document status
This is PACKAGING.md v1. It covers what shipped today: the
container, Type=notify systemd unit (with EXTEND_TIMEOUT_USEC
heartbeats so reindex doesn't fight the start timeout), OpenRC and
runit unit equivalents, on-disk layout, operational surface, release
pipeline, signing across all three surfaces, reproducible build via
Nix, CycloneDX SBOMs per binary, and a cargo-deny supply-chain gate.
Updated: 2026-05-07.
Binaries
satd ships two binaries:
| Binary | Purpose |
|---|---|
satd | Daemon. Long-running process; opens RocksDB, runs P2P + RPC + optional protocol surfaces. |
sat-cli | JSON-RPC CLI client. Bitcoin Core-compatible flag shape (-rpcuser, -rpcpassword, -rpccookiefile, network selectors). |
A third binary, sat-tui, is a curses-style operator dashboard. It is
optional; packagers who don't want it can skip it.
There are deliberately no separate sat-electrum / sat-esplora
companion binaries. Both protocols are subsystems of satd itself,
gated by runtime flags (--electrum=1, --esplora=1). One process,
one RocksDB, one log stream, one PID. This is a load-bearing design
choice — see the Native Protocol Architecture
chapter.
File layout
$DATADIR/ # default: $HOME/.bitcoin (Core-compat)
└── <network>/ # one of: <empty for mainnet>, testnet3, signet, regtest
├── blocks/
│ ├── blk00000.dat # flat-file block storage (state)
│ ├── blk00001.dat
│ └── ...
├── chainstate/ # RocksDB instance (state)
│ ├── *.sst # SST files — the bulk of disk usage
│ ├── CURRENT, MANIFEST-* # RocksDB metadata
│ └── ...
├── .cookie # RPC cookie auth (auto-generated, mode 0600)
├── mempool_history.log # rolling mempool snapshot (state, derived-OK)
├── reorg.log # persistent reorg ledger (state, append-only)
├── debug.log # rotating diagnostic log (derived)
├── bitcoin.conf # optional config file (Core-compat name)
└── satd.conf # alternative config name (also accepted)
State — must be backed up to preserve consensus history:
blocks/, chainstate/, reorg.log. These are load-bearing.
Derived / safe to nuke — regenerate from blocks/ via
--reindex or --reindex-chainstate: everything inside
chainstate/ (the RocksDB instance), mempool_history.log,
debug.log, and the various *.complete index marker files inside
chainstate/.
Single-instance RocksDB. Unlike Bitcoin Core, satd does not
maintain separate LevelDB databases for the txindex, address index,
or BIP 158 filter index. They are column families inside the one
RocksDB instance, written atomically with each connect_block
batch. This means:
- Backup is simpler (one directory).
- Index updates can never be visible without the corresponding tip
update — the whole
WriteBatcheither commits or it doesn't. - An
--reindex-chainstaterebuilds everything in chainstate (UTXO + indexes) but preserves the flat files.
Process model
- One process. PID file is whatever the supervisor records; satd does not write its own PID file by default.
tokioasync runtime; many tasks but a fixed-size worker pool.rayonfor script verification (CPU-bound parallelism).- File descriptors: RocksDB keeps many SST files mmapped; budget
LimitNOFILE=65536minimum. The systemd unit and the Docker image both pre-set this.
Signals
| Signal | Behaviour |
|---|---|
SIGTERM | Clean shutdown. Flush RocksDB, fsync undo files, drain mempool snapshot, close listeners. May take up to 10 minutes under heavy IBD load — most shutdowns are sub-second. |
SIGINT | Identical to SIGTERM. |
SIGHUP | Live config reload. Re-reads bitcoin.conf and applies the hot-reloadable subset without dropping the P2P swarm or flushing chainstate. satd logs to stdout (no debug.log), so SIGHUP is repurposed from Core's log-reopen to config reload — see Configuration, Tuning & Reload. |
SIGUSR1 | Live TLS certificate reload. Re-reads the configured TLS leaf cert/key from disk and swaps it in atomically on every TLS surface, without restarting or dropping connections. |
SIGKILL | RocksDB recovers via WAL replay on next start. Avoid; one botched shutdown = one corrupted chainstate is the failure mode to design against. |
Container supervisors should set a stop grace period of at least 10
minutes (--stop-timeout=600 for docker run, terminationGracePeriodSeconds: 600
for Kubernetes). The systemd unit ships TimeoutStopSec=10min for the
same reason.
Network ports (defaults)
| Service | Mainnet | Testnet | Signet | Regtest |
|---|---|---|---|---|
| P2P | 8333 | 18333 | 38333 | 18444 |
| JSON-RPC | 8332 | 18332 | 38332 | 18443 |
Esplora REST (--esplora) | configurable, e.g. 3000 | — | — | — |
Electrum (--electrum) | configurable, e.g. 50001 | — | — | — |
Metrics + health (--metricsport) | configurable, e.g. 9332 | — | — | — |
The default RPC bind is loopback. Esplora, Electrum, and the metrics endpoint are off by default; turn them on per-deployment.
Health and readiness
When --metricsport=<port> is configured, satd exposes three
unauthenticated HTTP endpoints on that port (default bind 127.0.0.1):
| Endpoint | Meaning |
|---|---|
GET /healthz | Process is alive and the event loop is responsive. Cheap. |
GET /readyz | RocksDB is open, headers are syncing, peers > 0. Returns 503 during IBD. |
GET /metrics | Prometheus exposition format. |
These are the right surfaces to wire to a Docker HEALTHCHECK,
Kubernetes liveness/readiness probes, or a systemd ExecStartPost=
poll. The shipped Type=notify unit (see §"systemd" below) uses
sd_notify(READY=1) for startup signalling; poll-based readiness
against /readyz works equally well for non-systemd supervisors.
Configuration
Two files are accepted, both with Bitcoin Core's key=value /
[network] syntax:
bitcoin.conf— Core-compat name. Same shape, same precedence.satd.conf— preferred when running side-by-side with a Core install; identical syntax.
Resolution order: --conf=<path> if given, else <datadir>/bitcoin.conf,
else <datadir>/satd.conf. CLI flags always win over file values.
The full flag matrix is in Configuration, Tuning &
Reload. The container ships a sensible
mainnet-loopback default; everything is overridable via -e SATD_* …
see "Container" below.
Container
The repository ships a multi-stage Dockerfile at the repo root.
Build:
docker build -t satd:dev .
Properties of the image:
- Base:
debian:bookworm-slim. - Runtime user:
satd(UID/GID 2121, deliberately non-1000 to avoid bind-mount UID clash with the host operator user). - PID 1:
tini, so SIGTERM forwards to satd cleanly. - Datadir:
/var/lib/satd. Marked as aVOLUME. - Exposed ports:
8333(P2P),8332(RPC). Other ports are off by default; map them with-pper deployment.
Example mainnet run with persistent state, RPC on loopback, metrics on loopback:
docker volume create satd-data
docker run -d --name satd \
--restart unless-stopped \
--stop-timeout 600 \
-v satd-data:/var/lib/satd \
-p 8333:8333 \
-p 127.0.0.1:8332:8332 \
-p 127.0.0.1:9332:9332 \
satd:dev \
--rpcbind=0.0.0.0 --rpcallowip=127.0.0.0/8 \
--metricsport=9332 --metricsbind=0.0.0.0
CLI:
docker exec satd sat-cli getblockchaininfo
Multi-arch images. Tag-triggered releases publish linux/amd64
and linux/arm64 to ghcr.io/epochbtc/satd via the workflow at
.github/workflows/release.yml. Tags follow docker/metadata-action
defaults: <MAJOR>.<MINOR>.<PATCH>, <MAJOR>.<MINOR>, and latest
on every release.
docker pull ghcr.io/epochbtc/satd:0.1.0
docker pull ghcr.io/epochbtc/satd:latest
These images are signed with cosign keyless OIDC, attested to the Rekor transparency log (verifier command under "Signed releases" below).
systemd
The repository ships contrib/systemd/satd.service. Install:
sudo install -Dm644 contrib/systemd/satd.service /etc/systemd/system/satd.service
sudo install -Dm755 target/release/satd /usr/local/bin/satd
sudo install -Dm755 target/release/sat-cli /usr/local/bin/sat-cli
sudo useradd --system --home /var/lib/satd --shell /usr/sbin/nologin satd
sudo systemctl daemon-reload
sudo systemctl enable --now satd
The unit ships with restrictive hardening (read-only /, private /tmp,
syscall filter, no new privileges). A packager who needs to relax any
of those — for example to write to a non-/var/lib/satd datadir —
should override via a drop-in:
# /etc/systemd/system/satd.service.d/datadir.conf
[Service]
ExecStart=
ExecStart=/usr/local/bin/satd --datadir=/srv/bitcoin
ReadWritePaths=
ReadWritePaths=/srv/bitcoin
The unit is Type=notify. satd calls sd_notify(READY=1) after every
listener (RPC, P2P, optional Esplora / Electrum / MCP / events
surfaces) is bound, so dependent units (Tor onion services pointing
at the RPC port, watchtower processes, monitoring agents) start at
the right moment instead of racing the bind sequence.
Reindex resilience
--reindex-chainstate on a fully-synced mainnet node runs for hours.
satd handles this without help from the operator:
- The unit sets a finite
TimeoutStartSec=3min— deliberately notinfinity. It is large enough for the first heartbeat (at 30s) to land and push the deadline out, and small enough that a pre-heartbeat startup wedge is killed in bounded time.EXTEND_TIMEOUT_USEConly works against a finiteTimeoutStartSec; an infinite startup timeout would let a wedged process hang indefinitely beforeREADY=1. - Every 30s during the pre-bind phase, satd emits
sd_notify(EXTEND_TIMEOUT_USEC=120000000, STATUS=...). TheEXTEND_TIMEOUT_USECresets systemd's internal kill-deadline; theSTATUSline shows live phase + progress insystemctl status satd. - The heartbeat IS the liveness check. If satd goes silent for >120s (genuinely stuck, not just slow), systemd kills the unit and the on-failure restart loop kicks in.
$ systemctl status satd
● satd.service — Bitcoin full node
Loaded: loaded (/etc/systemd/system/satd.service; enabled)
Active: activating (start) since Wed 2026-05-07 18:44:19 UTC
Status: "Replaying blocks (350000/800000, 43%)"
Main PID: 12345 (satd)
This is identical behaviour to Bitcoin Core's bitcoind.service since v22.
Running multiple networks side by side
Until a satd@.service template unit lands, operators who need
signet, regtest, and mainnet on the same host can copy the unit
under different names with per-instance drop-ins:
# Mainnet — the default unit installed above (satd.service).
# Signet on the same host:
sudo cp contrib/systemd/satd.service \
/etc/systemd/system/satd-signet.service
# /etc/systemd/system/satd-signet.service.d/instance.conf
sudo install -Dm644 /dev/stdin \
/etc/systemd/system/satd-signet.service.d/instance.conf <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/local/bin/satd --signet --datadir=/var/lib/satd-signet
StateDirectory=
StateDirectory=satd-signet
ReadWritePaths=
ReadWritePaths=/var/lib/satd-signet
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now satd-signet
Same pattern for --regtest. Each instance gets its own datadir, its
own satd-<network> user (or share the satd user — your call), and
its own RPC port (set via --rpcport=<n> in the drop-in).
A native satd@.service template unit (systemctl start satd@signet)
is a candidate for v0.1.x once we have signal that the drop-in pattern
isn't enough.
OpenRC
Alpine, Gentoo (with the openrc profile), Artix, and other distros
that use OpenRC. The repository ships contrib/openrc/init.d/satd.
sudo install -Dm755 contrib/openrc/init.d/satd /etc/init.d/satd
sudo install -Dm755 target/release/satd /usr/local/bin/satd
sudo install -Dm755 target/release/sat-cli /usr/local/bin/sat-cli
sudo adduser -S -H -h /var/lib/satd -s /sbin/nologin satd
sudo install -d -m 0750 -o satd -g satd /var/lib/satd
sudo rc-update add satd default
sudo rc-service satd start
OpenRC has no notify protocol — it considers the service "started"
once the daemon backgrounds via start-stop-daemon. That means
reindex doesn't fight any startup timeout the way it does on systemd;
the unit is just running for the entire reindex.
Per-instance config can be set via /etc/conf.d/satd:
# /etc/conf.d/satd
satd_args="--prune=550 --txindex=0"
runit
Void Linux, Artix-runit, and any s6-rc-compatible setup. The
repository ships contrib/runit/satd/run and a log helper at
contrib/runit/satd/log/run.
sudo install -Dm755 contrib/runit/satd/run /etc/sv/satd/run
sudo install -Dm755 contrib/runit/satd/log/run /etc/sv/satd/log/run
sudo install -Dm755 target/release/satd /usr/local/bin/satd
sudo install -Dm755 target/release/sat-cli /usr/local/bin/sat-cli
sudo useradd --system --home /var/lib/satd --shell /sbin/nologin satd
sudo install -d -m 0750 -o satd -g satd /var/lib/satd
sudo ln -s /etc/sv/satd /var/service/satd
runit supervises foreground processes; no daemonization, no readiness gate, no timeouts to fight with. Reindex runs as long as it needs to.
Resource budget
Mainnet, fresh IBD, no optional indexes:
| Resource | Pi 5 (8 GB) target | Server target |
|---|---|---|
| Disk (chainstate + blocks) | ~700 GB at 2026-05 tip | same |
| RAM peak during IBD | ~3 GB | unbounded by dbcache |
| RAM steady-state | ~1.5 GB | ~2 GB |
| CPU during IBD | 4 cores ≈ saturated | scales with cores |
| Network during IBD | 50–200 Mbps | network-bound |
Optional indexes (--txindex, --addressindex, --blockfilterindex)
each add disk and a one-time backfill cost. Turning them on after the
fact runs an online backfill — there is no "stop, reindex from scratch"
ceremony; see node/src/index/<index>/backfill.rs for the cursors.
Pruning
--prune=<MiB> works the same shape as Bitcoin Core. Minimum 550 MiB.
Indexes that scan historical blocks (--txindex, --addressindex,
--blockfilterindex) require unpruned blocks. satd refuses to start
with a conflicting combination — same shape as Core.
Reproducible build via Nix
The repo ships a Nix flake at flake.nix that produces deterministic
satd and sat-cli binaries on x86_64-linux and aarch64-linux.
Quickstart for a packager who already has Nix with flakes enabled:
# Build (produces ./result/bin/{satd, sat-cli})
nix build .#satd
# Hash the built binaries
sha256sum result/bin/satd result/bin/sat-cli
# Drop into a dev shell with the full toolchain (clang, libclang,
# cmake, openssl, rustc, cargo, rustfmt, clippy, cargo-watch,
# cargo-nextest)
nix develop
The toolchain pin (rust-toolchain.toml at the repo root) is the
single source of truth — both rustup and the flake read it.
What "reproducible" means in v1
- Two
nix buildinvocations of the same commit on two hosts produce byte-identicalresult/bin/satd. CI proves this on every PR that touchesflake.nix/flake.lock/rust-toolchain.toml/Cargo.lockvia.github/workflows/nix.yml(a two-runner pair build + a third compare job that asserts SHA256 equality). - Local repro is one command:
contrib/repro/diff-build.sh /path/to/clone-A /path/to/clone-B. Runsnix buildin each, hashes the outputs, and falls back todiffoscopewhen they diverge. - Out of scope for v1: matching the rustup-stable tarball binary
(the one
.github/workflows/release.ymlships) byte-for-byte. That requires aligning linker / debug-info / build-id behaviour between two different build drivers; tractable but a separate PR.
Determinism hazards addressed
| Hazard | How the flake handles it |
|---|---|
rocksdb-sys bindgen output | rustPlatform.bindgenHook sets up libclang + the stdenv's system include paths so bindgen's translation-unit parse is reproducible. Output is deterministic for a fixed libclang version. |
| RocksDB native code | We use nixpkgs's pre-built rocksdb (via ROCKSDB_LIB_DIR / ROCKSDB_INCLUDE_DIR) rather than the librocksdb-sys-vendored C++ tree. nixpkgs builds rocksdb portably (no -march=native), so cross-runner CPU variance is a non-issue. The tradeoff is a minor version mismatch between librocksdb-sys's pinned 10.4.2 and whatever nixpkgs ships (regenerated bindings either way; major API drift would surface as a compile error). |
cc-rs C/C++ compiles (secp256k1, bitcoinconsensus) | Compiler version pinned via nixpkgs; SOURCE_DATE_EPOCH respected by cc-rs for any timestamped output. |
OUT_DIR paths in generated code | crane builds inside a content-addressed /build/source; paths are stable across hosts. |
| Linker build-id | RUSTFLAGS=-C link-arg=-Wl,--build-id=none drops the per-build random ID. |
Cargo --release profile | CARGO_PROFILE_RELEASE_STRIP=symbols strips deterministically inside the derivation. |
tonic_build / proto generation | events/proto/*.proto files included in the source filter; protoc is vendored via protoc-bin-vendored so no host protoc dep. |
Gating policy
The Nix workflow runs on tag pushes (v*), workflow_dispatch,
and PRs that change flake-specific files (the flake itself,
rust-toolchain.toml, the workflow, the repro helper under
contrib/repro/). It deliberately does not trigger on
Cargo.lock / Cargo.toml edits — every dependency bump touches
those and burns hosted-runner minutes for low-signal runs.
The Nix and Release workflows fire in parallel at tag-cut time and don't gate each other; a Nix-side failure means the released tarball can't claim Nix-rebuilt provenance for that tag and should be fix-forwarded.
Reconsider both the trigger scope (broader PR-trigger) and a hard
Release-gates-on-Nix dependency once the repo flips public (Actions
minutes free).
flake.lock
The first PR that lands the flake intentionally does not commit
flake.lock because the maintainer who lands it does not have Nix
on their workstation. The CI workflow is gated to workflow_dispatch
- flake-touching PRs + tag pushes; the first
workflow_dispatchrun by a Nix-capable maintainer (or from a CI runner) will generate the lock, after which it should be committed and the PR description updated. Subsequent PRs run against the committed lock.
Renovate (or a manual cadence) bumps the lock weekly. Bumps that
change flake.lock re-trigger the repro check; if reproducibility
breaks under a new input revision, the bump is reverted and the
hazard is investigated.
What's intentionally not in this flake
- macOS reproducibility (
aarch64-darwin) — deferred until the repo flips public; macOS builds in the release workflow are also currently disabled for the same reason. - musl targets — same
rocksdb-sys + muslcross-toolchain reason the release workflow defers them. - A NixOS module / Home Manager output — packagers should write their own service definitions; the contract this doc describes is the input.
- A maintainer-owned binary cache (Cachix) — adds a key-custody
surface we're not taking on yet. The CI uses the ephemeral
magic-nix-cacheaction for speedup only.
Bitcoin Core uses Guix; satd targets Nix as the primary reproducible build because the workspace is pure-Cargo and Nix integration is substantially shorter to specify. A Guix manifest may follow if a downstream packager needs it.
Release artifacts
Tag-triggered (v*) releases produce, per tag, via
.github/workflows/release.yml running on hosted GitHub runners:
-
satd-<version>-<target>.tar.zstfor the targets currently shipped:x86_64-unknown-linux-gnuaarch64-unknown-linux-gnux86_64-unknown-linux-musl(statically-linked musl)aarch64-unknown-linux-musl(statically-linked musl)aarch64-apple-darwin(macOS Apple Silicon)
x86_64-apple-darwinis not built in the standard release matrix — macos-13 is being deprecated by GitHub and Apple Silicon is the targeted macOS surface. Operators who need x86_64 darwin can cross-compile from an arm64 darwin host (cargo build --release --target=x86_64-apple-darwin).Each tarball contains stripped
satd+sat-clibinaries and the authoritative reference docs (README.md,PACKAGING.md,CORE_DIFFERENCES.md,STABILITY_POLICY.md), plus aMANIFESTfile pinning the build commit, target triple, Rust toolchain version, and build timestamp. -
A per-tarball
*.sha256file alongside each artifact, plus an aggregateSHA256SUMScovering tarballs and SBOMs in the release. -
A multi-arch container at
ghcr.io/epochbtc/satd:<version>coveringlinux/amd64+linux/arm64. -
CycloneDX 1.5 JSON SBOMs for each shipped binary:
satd-v<version>.cdx.jsonsat-cli-v<version>.cdx.json
Each ships with a
*.sha256next to it (already inSHA256SUMS) and a*.minisigproduced by the same maintainer-sidecontrib/release/sign-tarballs.shflow that signs the tarballs.
The release workflow is triggered on tag pushes (v*) and manual triggers (workflow_dispatch), building the full set of binary, container, and SBOM artifacts in parallel.
Signed releases
Three independent signing surfaces. Verifier commands and key custody
details live in SECURITY.md.
-
Tarballs — minisign Ed25519. Each
.tar.zstships with a detached.minisig. Pubkeys (primary + cold spare) inSECURITY.md. Maintainer signs offline with passphrases gated by 1Password + YubiKey 2FA — the signing key is never present in CI. Maintainer runbook:contrib/release/sign-tarballs.sh <tag>. -
Container image — cosign keyless OIDC. No signing key in custody. The merge-manifest CI job mints a short-lived cert from GitHub Actions OIDC and the attestation is logged to Rekor. Verify with:
cosign verify ghcr.io/epochbtc/satd:<version> \ --certificate-identity-regexp \ 'https://github.com/epochbtc/satd/.github/workflows/release.yml@refs/tags/v.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com -
Git tags — SSH signatures. Annotated tags are signed by the maintainer's SSH key. Source-of-truth for the trusted pubkey set is
https://github.com/bkeroack.keys(delegating to GitHub avoids a stale pinned file as machines rotate). Verify with the bundled helper:contrib/release/verify-tag.sh v0.1.0
Software Bill of Materials
Each release ships a CycloneDX 1.5 JSON SBOM per binary:
# Authenticate the SBOM (same key + recipe as the tarballs)
minisign -Vm satd-v0.1.0.cdx.json \
-P RWQeP6MczCgPh6tU03GEMm4HsnGbXte3VT2Bc52TBSR7Q+X7WnL5vfQ3
# Enumerate dependencies — name, version, license
jq -r '.components[] | "\(.name) \(.version) \(.licenses[0].license.id // .licenses[0].license.name // "?")"' \
satd-v0.1.0.cdx.json | sort
The SBOM is generated from the same Cargo.lock that produced the
released binary; the cargo cyclonedx invocation lives in the sbom
job in .github/workflows/release.yml. The dep graph is identical
across the gnu-linux release targets currently shipped
(x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu), so a
single SBOM per binary covers both tarballs.
If a future release adds musl or macOS targets — which can resolve
different platform-specific deps (e.g. libc shim crates,
security-framework on darwin) — the workflow will need to emit a
per-target SBOM and the artifact filenames will gain a target-triple
suffix. Track this when re-enabling the deferred targets in the
release matrix.
Supply-chain policy
deny.toml at the repo root encodes the supply-chain policy enforced
by cargo-deny:
- Advisories — every RustSec advisory against any dep in the
workspace fails CI by default. Exceptions are documented in
[advisories.ignore]with areasonfield naming the rationale. - Licenses — permissive only (MIT / Apache-2.0 / BSD / ISC / Unicode / CC0 / Zlib / Unlicense / MPL-2.0 family). GPL-* and AGPL-* are denied implicitly.
- Bans — wildcards on crates.io deps are denied; workspace-internal
path = "../foo"deps are allowed viaallow-wildcard-pathsbecause every workspace crate ispublish = false. - Sources — only
https://github.com/rust-lang/crates.io-index. Git deps require an explicit allowlist entry.
The policy runs as a hard gate in two places:
.github/workflows/deny.yml— every PR that touchesCargo.toml,Cargo.lock,deny.toml, or the workflow itself.- The
supply-chain-gatejob inside.github/workflows/release.yml— every release artifact (tarballs, SBOMs, container)needs:it. A new RustSec advisory landed during a quiet period between merges cannot ship a release.
Known deferred items
cargo-auditable— Embedding the dependency manifest directly into the compiled binaries for improved runtime supply-chain verification.
Stability contract
Shipped surfaces (RPC method shapes, CLI flag shape, bitcoin.conf
syntax, file layout described above) are governed by
STABILITY_POLICY.md. Tier 1 (Core-compat) is the strongest: a
breaking change requires a deliberate, scoped proposal with a
demonstrated migration story for downstreams.
Packaging contacts
If you are packaging satd for an ecosystem (Umbrel, Start9, Debian,
Nix, Homebrew, etc.) and need a contract change, file an issue tagged
packaging against the epochbtc/satd repo. We treat packaging
breakage as a P1.
Versioning
This document is versioned alongside satd. Changes that shift the contract (file layout, signals, default ports) are called out in the release notes for the version that ships them.
| Version | Notable changes |
|---|---|
| 0.1.0 (current) | Initial PACKAGING.md. Dockerfile + systemd unit shipped. Tag-triggered release workflow on hosted runners produces tarballs (gnu-linux + Apple Silicon) and a multi-arch GHCR image. Signing across all three surfaces (minisign tarballs, cosign keyless image, SSH-signed tags) shipped. Nix flake (flake.nix) shipped for reproducible builds with two-runner CI verification (x86_64-linux, aarch64-linux). CycloneDX 1.5 SBOMs per binary + cargo-deny supply-chain gate (PR-time on dep-graph PRs, hard gate at tag time) shipped. systemd unit upgraded to Type=notify with sd_notify heartbeats so --reindex-chainstate doesn't fight TimeoutStartSec; OpenRC and runit unit equivalents shipped. |