Thanks for your interest. satd is a Bitcoin Core-compatible full node in Rust; the project's stability and correctness expectations are unusually high because downstream packagers (Umbrel, Start9, BTCPay, distros) and end-user wallets depend on its surfaces. This document covers what that means in practice for contributors.
- Security issues — do not open a public issue or PR. Disclosure
process is in
SECURITY.md. - Surface changes — anything that touches an RPC method shape, CLI
flag,
bitcoin.confkey, file layout, or release-artifact name is governed bySTABILITY_POLICY.md. Tier 1 (Bitcoin-Core-compatible) surfaces have the strongest contract; please read the policy before proposing changes there. - Operator-facing changes — if a flag, default, or signal changes
behaviour the operator has to know about, update the operator manual
(
docs/manual/) in the same PR.
- Fork the repo or create a branch (
feat/<topic>,fix/<topic>,chore/<topic>,docs/<topic>). - Make focused commits. We prefer small, reviewable PRs over megapatches.
- Run the local checks (below) before pushing.
- Open a PR against
master. Describe the motivation, the change, and how you verified it. If your change touches a Tier 1 surface, call that out explicitly and reference the relevantSTABILITY_POLICY.mdsection. - CI must be green for a PR to be merged. CI runs the same checks listed
below plus
cargo-denyon dep-graph-touching PRs.
Two appliance jobs are release gates rather than per-PR ones, because each
compiles satd from scratch and they cost around forty minutes of runner time
between them: the reference-stack bring-up and the appliance image build.
They run on release tags and on demand. If your change touches
contrib/stack/ or contrib/appliance/, add the appliance-ci label to
your PR to run them there — applying the label starts the run, so no extra
push is needed. The cheap static checks over those directories run on every
PR regardless.
Stacked PRs are fine. State the merge order in each PR description and land them in that order.
Before pushing, run:
cargo fmt --all -- --check
cargo clippy --all-features --all-targets -- -D warnings
cargo test --workspace --all-featuresIntegration tests live in satd/tests/ and exercise a real regtest node;
they take a few minutes. Library-only iteration is fine with
cargo test -p <crate>, but the regtest suite must pass before opening a
PR that touches consensus, mempool, RPC, or P2P code.
- One-line summary in the imperative ("add foo", not "added foo" or "adding foo"). Under ~70 chars.
- Optional body explaining motivation and trade-offs.
- Reference issues with
Fixes #NorRefs #N. - Subsystem prefixes are conventional but not required — examples:
node:,rpc:,mempool:,electrum:,esplora:,packaging:,docs:.
Maintainers SSH-sign commits and tags (ssh-keygen -Y sign); contributors
are not required to. Verification details are in SECURITY.md.
- Edition 2024 Rust.
cargo fmtis the source of truth. clippyruns with-D warningsin CI; fix or#[allow]with a brief justification comment.- Errors use
thiserror. Nounwrap()/expect()in non-test code unless an invariant proof is in a comment. - All async code uses
tokio. Nostd::threadfor I/O. - Storage writes use
StoreBatchfor atomicity. - P2P uses the
bitcoincrate'sNetworkMessagetypes directly; no bespoke wire format.
- Pure logic — unit tests next to the code (
#[cfg(test)] mod tests). - Anything that crosses a subsystem boundary — integration test in
satd/tests/regtest.rsagainst a real regtest harness. - New RPC methods and Esplora / Electrum endpoints — integration test asserting the wire shape for a representative request.
- Consensus-adjacent changes — extend the shadow-verifier coverage if
applicable; mismatches must produce a structured
ShadowMismatch. Script evaluation is shadow-checked againstlibbitcoinconsensus; the block-acceptance pipeline around it is covered by the differential battery — static fixtures innode/tests/feature_block_consensus.rsand the live-Core differential / fuzzer (satd/tests/core_block_differential.rs,fuzz/). If your change touches block acceptance (PoW, commitments, sigops, BIP 34, value conservation, maturity, timestamps, locktime), add or extend a case there.
AI-assisted contributions are welcome. The bar is the same as any other contribution: the engineering has to be solid, the diff has to be one you've read and understood, and you take responsibility for what you submit. We don't require disclosure that an AI tool was used.
What we do expect:
- Run the local checks on your own machine. Don't trust an AI tool's
claim that
cargo testpasses — run it. AI tools routinely hallucinate clean output, and "it compiled in the AI's head" is not the same as "it compiles." - Read the diff line by line before pushing. Watch for: invented APIs
(functions, methods, or crate features that don't exist), stubs the AI
left behind from its own planning, tests that exercise the wrong path
or assert against constants the implementation just produced,
unwrap/expectsneaked into non-test code, drive-by reformatting of unrelated lines. - Submit changes you could have written or debugged unaided. If you can't reason about why the diff is correct, you can't defend it in review and you can't maintain it after merge. Smaller PRs you fully understand beat sprawling PRs you don't.
- The PR description states what the change does and how you verified it. Reviewers will close PRs that read as unreviewed AI output.
Subsystems with sharper edges (consensus / verifier / connect-block, RocksDB schema, P2P state machine, sighash and signature-verification code) are the wrong place to learn — please get familiar with the surrounding code before asking an AI to edit it.
- PRs that add features without an accompanying integration test.
- PRs that alter Tier 1 surfaces without a
STABILITY_POLICY.mdanalysis. - PRs that depend on unpublished or git-only crate versions.
- PRs that reformat files unrelated to the change.
By contributing, you agree your contributions are licensed under the
project's MIT licence (see LICENSE).