Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e9bf7f2
ci: add gh actions to run tests (#11)
rafa-stacks Aug 11, 2026
d6fe97a
chore: group rust files by command (#12)
rafa-stacks Aug 11, 2026
7f8d2b3
test: add unit tests for utils (#13)
rafa-stacks Aug 11, 2026
3b1652e
chore: add clippy ci step (#14)
rafa-stacks Aug 11, 2026
eb66638
tests: add tests to all remaining modules (#15)
rafa-stacks Aug 11, 2026
b115f2c
feat: add standalone network definition files (#16)
rafa-stacks Aug 11, 2026
68dcc2c
feat: add per-service `image` override to stacks.toml (#19)
rafa-stacks Aug 11, 2026
c80aa56
feat: gate chainstate download on the archive's postgres version (#20)
rafa-stacks Aug 11, 2026
6636957
feat: user-owned secrets.toml overlay merged at load time (#21)
rafa-stacks Aug 12, 2026
bece69d
feat: chainstate download --start boots the deployment after restore …
rafa-stacks Aug 13, 2026
afb1095
feat: split compose into bitcoin/core/services networks (#23)
rafa-stacks Aug 14, 2026
17f5938
feat: run multiple deployments per machine via name and port_offset (…
rafa-stacks Aug 15, 2026
235e41c
feat: start preflight guards and external-node event-port warnings (#25)
rafa-stacks Aug 15, 2026
34be5cc
fix: render secret-bearing service configs with 0600 permissions (#28)
rafa-stacks Aug 17, 2026
bf96bd1
chore: rework console comments (#29)
rafa-stacks Aug 18, 2026
67b194a
ci: release flow with docker and binary artifacts (#30)
rafa-stacks Aug 18, 2026
bdc8ed9
feat: add support for signer-sidekick (#31)
rafa-stacks Oct 1, 2026
772b4af
feat: add staking-testnet as a built-in network (#32)
rafa-stacks Oct 1, 2026
8e799fd
fix: use salt from username only
rafa-stacks Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CI

on:
pull_request:
push:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always

jobs:
test:
name: Build & test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Install Rust toolchain from rust-toolchain.toml
run: |
rustup toolchain install
rustup show

- name: Cache cargo
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2

- name: Build
run: cargo build --locked

- name: Run tests
run: cargo test --locked

clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Install Rust toolchain from rust-toolchain.toml
run: |
rustup toolchain install
rustup component add clippy
rustup show

- name: Cache cargo
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2

# Warnings are errors in CI so lints can't accumulate silently.
- name: Run clippy
run: cargo clippy --all-targets --locked -- -D warnings
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
stacks.toml
/.export-tmp
.DS_Store
/secrets.toml
14 changes: 13 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ anyhow = "1"
clap = { version = "4", features = ["derive"] }
colored = "2"
flate2 = "1"
getrandom = "0.2"
hmac = "0.12"
indicatif = "0.17"
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
Expand Down
79 changes: 77 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ stacksup chainstate status # compare every service's chain tip (stacks + bitcoin
stacksup chainstate download # seed chainstate from the Hiro Archive (resumable, verified)
```

## Networks

Standard network definitions (burnchain endpoint, chain id, epochs, seeded
balances, bootstrap peers) live in [`networks/`](networks/) and ship embedded
in the binary — `network = "testnet"` in stacks.toml references them by name.
A new testnet (new chain id, new epochs) is a new file there, not a config
migration. Unknown names resolve as custom definition files next to your
stacks.toml (`networks/<name>.toml`), so you can define private networks
without a tool release.

## The model

`stacks.toml` is the single source of truth. Every service has a `mode`:
Expand All @@ -47,6 +57,69 @@ be configured to *push* to them; `stacksup config render` emits
`rendered/apply-to-your-node.toml` with the exact blocks to add on your side,
and `stacksup config check` verifies the loop is closed.

Containers are segmented across three docker networks so a compromised
API-side container has no route to the signer or to bitcoind's RPC
interface: `bitcoin` (bitcoind + node), `core` (node + signer), and
`services` (API, mesh API, Postgres). The node joins each network only when
it's present — everything talks to the node. bitcoind's RPC port is
published loopback-only (unless the node is external and needs it
off-host), so containers can't sidestep the split via
`host.docker.internal`; the P2P port stays open on purpose — it exists to
accept peers from anywhere.

## Running multiple deployments

One machine can host several stacks side by side. Give each deployment its
own directory (config + `--data-dir`), a distinct `name`, and a
`port_offset`:

```toml
name = "testnet-b" # compose project + container prefix (default: "stacks")
port_offset = 100 # shifts every published HOST port; container-internal
# ports and service wiring never change
```

With `port_offset = 100` the node RPC publishes on 20543, the API on 4099,
postgres on 5532, and so on. The rendered compose file embeds the project
name, so `stacksup` commands (and bare `docker compose -f` runs) are always
scoped to the deployment whose directory you're in — `stop`, `logs`, and
`chainstate wipe` can't touch a neighbour.

`stacksup start` refuses to run before doing damage when it detects a
collision: it test-binds every port it is about to publish (pointing at
`port_offset` when one is taken) and rejects a `name` already in use by a
stack rendered from a different directory (which compose would otherwise
silently adopt).

## Secrets

Credentials never live in `stacks.toml` — the tool rejects them there. They go
in a `secrets.toml` beside it (plain text, mode 0600, gitignored), which is
merged into the config at load time:

```toml
[postgres]
password = "..."

[bitcoind]
rpc_user = "..."
rpc_password = "..."

[stacks-node]
auth_token = "..."
```

`stacksup config init` generates one with random values when none exists;
an existing `secrets.toml` is yours and is **never modified or overwritten**
(not even by `init --force`) — if a required value is missing, the tool errors
out with a paste-ready snippet of exactly what to add. The file must be
owner-only (`chmod 600`), and values must be 8–128 characters of printable
ASCII without spaces, quotes, backslashes, `$`, or backticks (they are
interpolated into rendered TOML/env/compose files). At render time the
Postgres password is delivered as a compose secret file and the bitcoind
credentials become a derived `-rpcauth` hash, so no plain-text secret appears
in `docker inspect`.

## Development

```bash
Expand All @@ -64,15 +137,17 @@ API). Downloads are resumable — Ctrl-C and re-run any time. Useful flags:
`--service node|api|all`, `--archive <file|url|path>` to pin a specific
archive, `--check-only` for a dry-run plan, `--yes` for unattended runs
(`nohup stacksup chainstate download --yes &`), `--no-verify`,
`--skip-version-check`, `--keep-archives`. Always resolves versioned archives (never -latest pointers); the archive's version must be ≤
`--skip-version-check`, `--keep-archives`, and `--start` to render and
start the deployment as soon as the restore finishes (seed + boot in one
command: `stacksup chainstate download --yes --start`). Always resolves versioned archives (never -latest pointers); the archive's version must be ≤
the service's configured `version` in stacks.toml.

## Roadmap

- [ ] `status --watch`: live sync progress (bitcoind headers, node tip vs peers via `/v3/health`, API ingest lag) via bollard
- [ ] Snapshot seeding on first `up`: Hiro archive chainstate + matching API pg_dump, resumable, checksummed
- [ ] `doctor`: chain-id cross-checks, event-stream-flowing check, node↔signer auth verification
- [ ] Secrets: generated per-stack tokens/passwords in a gitignored env file (currently dev defaults — do not use on mainnet)
- [x] Secrets: user-owned `secrets.toml` overlay beside stacks.toml — pg password via compose secret file, bitcoind via rpcauth hash, node/signer auth token
- [ ] `upgrade`: image update with pre-upgrade pg backup, ordered restart, post-check
- [ ] `snapshot`: stop-consistent chainstate + pg_dump pairs with version metadata
- [ ] Profiles: `exchange` (readonly API replicas, pruned mode), richer `signer` (monitor-signers wiring)
Expand Down
15 changes: 15 additions & 0 deletions networks/mainnet.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Network definition: Stacks mainnet.
# Referenced from stacks.toml as `network = "mainnet"`.

name = "mainnet"
chain_id = 0x00000001
hiro_archive_path = "mainnet"

[bitcoind]
allow_managed = true
chain = "main"
rpc_port = 8332
p2p_port = 8333

[node]
burnchain_mode = "mainnet"
128 changes: 128 additions & 0 deletions networks/testnet.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Network definition: Stacks testnet (krypton), following the Hiro-hosted bitcoin regtest.
# Referenced from stacks.toml as `network = "testnet"`.

name = "testnet"
chain_id = 0x80000000
hiro_archive_path = "testnet"

[bitcoind]
allow_managed = false
chain = "test"
rpc_port = 18443
p2p_port = 18444
default_host = "bitcoin.regtest.hiro.so"

[node]
burnchain_mode = "krypton"
bootstrap_node = "0348af7ce1b224476e8f042727af3f84dcf49a69bb3c9dd2a1afaa783acfffb729@seed.testnet.hiro.so:20444"
pox_prepare_length = 100
pox_reward_length = 900
node_extra = '''
pox_5_sbtc_contract = "SN3VMHXEN64ZZF71JQ5VESXDWTR301XTTXGF4J8F1.sbtc-token"
pox_5_sbtc_registry_contract = "SN3VMHXEN64ZZF71JQ5VESXDWTR301XTTXGF4J8F1.sbtc-registry"
pox_5_bond_admin = "ST1V2ASRWGR81W7GBN1Z4W2JQKXJWCADPVZG30X45"
'''

[[ustx_balance]]
address = "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2"
amount = 10000000000000000

[[ustx_balance]]
address = "ST319CF5WV77KYR1H3GT0GZ7B8Q4AQPY42ETP1VPF"
amount = 10000000000000000

[[ustx_balance]]
address = "ST221Z6TDTC5E0BYR2V624Q2ST6R0Q71T78WTAX6H"
amount = 10000000000000000

[[ustx_balance]]
address = "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B"
amount = 10000000000000000

[[ustx_balance]]
address = "ST31XHNM0GZ2K978FPP4QA3STNQ73Z8C9G9MJEPK2"
amount = 10000000000000000

[[ustx_balance]]
address = "ST1B38CGQRPXEMRH7B66VXTS22DQTNMSW4YJJ7QK1"
amount = 10000000000000000

[[ustx_balance]]
address = "STDMN71Z0H9EF8CRKAWTGBB5YS0BNV26HZ79QFFP"
amount = 1000000000000000

[[ustx_balance]]
address = "ST1E0PSCH72JMQH9QCH293ZTEEH7BPA40Y3F39XQ"
amount = 10000000000000

[[ustx_balance]]
address = "ST3QBTK0Q438YVNX8EG6Z85HN0WKQPXYT25H5SPPK"
amount = 10000000000000

[[ustx_balance]]
address = "ST10BX04F9PC6N1WBXKW3H7CG0NS0A3PK650T3P3R"
amount = 10000000000000

[[ustx_balance]]
address = "ST3AF1BBQAFSFCM8K4ZBR1FBXP3P8J1CKGSGDHWR5"
amount = 100000000000000

[[ustx_balance]]
address = "STHY13V44422NAN6D3NSJPY9CDR3ED1M6HH9WZ6Y"
amount = 10000000000000

[[epochs]]
epoch_name = "1.0"
start_height = 0

[[epochs]]
epoch_name = "2.0"
start_height = 0

[[epochs]]
epoch_name = "2.05"
start_height = 1

[[epochs]]
epoch_name = "2.1"
start_height = 2

[[epochs]]
epoch_name = "2.2"
start_height = 3

[[epochs]]
epoch_name = "2.3"
start_height = 4

[[epochs]]
epoch_name = "2.4"
start_height = 5

[[epochs]]
epoch_name = "2.5"
start_height = 6

[[epochs]]
epoch_name = "3.0"
start_height = 1802

[[epochs]]
epoch_name = "3.1"
start_height = 1803

[[epochs]]
epoch_name = "3.2"
start_height = 1804

[[epochs]]
epoch_name = "3.3"
start_height = 1805

[[epochs]]
epoch_name = "3.4"
start_height = 1806

[[epochs]]
epoch_name = "4.0"
start_height = 2702
Loading
Loading