Nix is not required to clone, compile, run --smoke, or send a PR.
CI pins rustc 1.95.0; rust-toolchain.toml makes
rustup install that channel (plus rustfmt and clippy)
the first time you run cargo in this tree.
Operator Linux binaries stay the musl path in README.md /
OPERATOR.md. This section is developing the tree.
| Need | Why | Typical install |
|---|---|---|
| Git | Clone this repo. Do not git clone --recurse-submodules — that would pull all of Bitcoin Core. Consensus tests sparse-clone ~16 MiB via scripts/core-functional/init-submodule.sh. |
git-scm.com (Windows: Git for Windows, includes Git Bash) |
| Rust 1.95 via rustup | Workspace rust-version / CI. Distro apt install rustc / Homebrew rust will not honor rust-toolchain.toml. |
rustup.rs — then cd into the clone; rustc --version should print 1.95.0 |
| C compiler | rbitcoin-node / rbitcoin-cli link mimalloc (cc crate). rust-bitcoin's secp256k1 also compiles C. |
Linux: gcc or clang (build-essential on Debian/Ubuntu). macOS: xcode-select --install. Windows: MSVC Build Tools ("Desktop development with C++"); rustup's default host is x86_64-pc-windows-msvc |
| Bash | Helper scripts/*.sh and the Windows/macOS CI smoke. |
Linux/macOS: /bin/bash. Windows: Git Bash (same shell CI uses). PowerShell can cargo build but not those scripts |
First cargo also fetches crates.io deps. Keep several GiB free for target/.
git clone https://github.com/reardencode/rbitcoin.git
cd rbitcoin
# rustup reads rust-toolchain.toml (1.95.0 + rustfmt + clippy)
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$PWD/target/dev}" # matches CI; optional
rustc --version # expect 1.95.0
cargo build -p rbitcoin-node -p rbitcoin-cliWindows cmd: set CARGO_TARGET_DIR=target\dev then the same cargo build.
PowerShell: $env:CARGO_TARGET_DIR = "target/dev". Git Bash: same export as above.
Binaries land at $CARGO_TARGET_DIR/debug/rbitcoin-node (.exe on Windows).
Without CARGO_TARGET_DIR, cargo uses target/debug/.
Same flag the required windows / macos CI jobs run:
# Unix — datadir is disposable
./target/dev/debug/rbitcoin-node --smoke --network regtest \
--datadir /tmp/rb-smoke --no-seeds --log-level errorREM Windows cmd (adjust path if you did not set CARGO_TARGET_DIR)
target\dev\debug\rbitcoin-node.exe --smoke --network regtest --datadir %TEMP%\rb-smoke --no-seeds --log-level errorDo not point --datadir at a mainnet tree for this. Next: first-hour
regtest (mine → Electrum → Esplora) in docs/operator/operations.md
using this cargo binary (not the musl target/release/ install).
Linux is the supported operator IO target (io_uring when the ring opens).
Darwin defaults to libc pread (RBITCOIN_IO=pool opts into the pool session); Windows to IOCP (no IoRing). Matrix:
docs/io-modality.md. PR CI still smokes native store
IO on Windows and Darwin.
| You want | Linux | macOS | Windows |
|---|---|---|---|
cargo build -p rbitcoin-node -p rbitcoin-cli |
yes | yes | yes |
rbitcoin-node --smoke |
yes | yes | yes |
./scripts/ci-os-smoke.sh (store platform + mmap fuse + query confirm + --smoke) |
yes | yes — this is the required macos job |
Git Bash — this is the required windows job |
cargo fmt --all / cargo clippy --workspace --all-targets -- -D warnings |
yes | yes | yes |
cargo test --workspace (required test job) |
yes | usually (see flock below) |
use WSL2 (Ubuntu + rustup inside the distro). Native CI does not run the full workspace suite |
./scripts/coverage.sh / cargo deny / ./scripts/ast-grep.sh |
yes (install extras below, or Nix) | possible; not the CI host | not the supported path |
nix develop / nix-shell |
optional pin (below) | optional pin (below) — aarch64-darwin |
no |
macOS Intel vs Apple Silicon: rustup installs a native toolchain. CI macos is
macos-26 (aarch64). You can develop on either; Release Darwin zips are aarch64
only and ad-hoc signed (OPERATOR.md).
WSL2: treat it as Linux. Install rustup inside the distro (not the Windows
one), clone on the Linux filesystem (~/…, not /mnt/c/…) so IO and line
endings stay sane. If Git Bash .sh scripts fail with $'\r' / "bad
interpreter", the clone used CRLF — git config core.autocrlf input and
re-checkout.
An editor with rust-analyzer at the repo root picks up rust-toolchain.toml.
cargo test for consensus Core vectors stages Bitcoin Core v31.1
src/test/data/*.json from third_party/bitcoin. If that tree is missing,
tests run scripts/core-functional/init-submodule.sh
(sparse clone, ~16 MiB). The script needs git, bash, and flock:
- Linux:
flockis from util-linux (usual distro default). - macOS: not always present —
brew install flock(or run the script after that package is onPATH). - Windows: Git Bash often has no
flock; nativeCommand::newon a.shalso fails. Populate the sparse checkout from WSL or skip to store/node smoke. Do notgit submodule update --init(full Core clone).
You can also run the script yourself before the first consensus test:
./scripts/core-functional/init-submodule.shexport CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$PWD/target/dev}"
cargo fmt --all # includes nested fuzz/ crate
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p rbitcoin-primitives --test suite # fast sanity
cargo test -p rbitcoin-store --test suite # store (tests pass Tiny at open; production default is Mainnet)
cargo test --workspace # default suite — Linux / macOS; see table
./scripts/ci-os-smoke.sh # Windows/macOS PR surfaceSuite tiers, budgets, coverage: TESTING.md. Design:
docs/architecture.md. Map: docs/README.md.
Not needed to compile or --smoke. CI installs prebuilts; locally:
| Tool | Local | Job |
|---|---|---|
| cargo-deny | cargo install cargo-deny --locked then cargo deny check |
deny |
| ast-grep | ast-grep.github.io (or Nix) then ./scripts/ast-grep.sh |
ast-grep |
| cargo-llvm-cov | cargo install cargo-llvm-cov --locked + rustup component add llvm-tools-preview then ./scripts/coverage.sh |
coverage |
flake.nix devShells cover x86_64-linux, aarch64-linux, and
aarch64-darwin. They pin rustc to the same flake.lock as musl release builds
and set CARGO_TARGET_DIR=target/dev plus RUSTFLAGS=-Dwarnings:
nix develop # or nix-shell — both read flake.lock, not floating <nixpkgs>That is not the operator binary (nix build .#rbitcoin-musl). Details:
docs/reproducible-builds.md.
Release packages stay Linux-only (nix build has no aarch64-darwin
attribute). The dev shell pins tool versions and builds nothing that ships, so
the static-musl constraint that excludes Darwin from packages does not apply
to it. Darwin operator binaries come from the macos-26 release job, not Nix.
Write clear, concrete technical English. Keep Core-aligned terms where we match Bitcoin Core. Do not inject moralizing or political framing, or soften consensus and security language. Keep a name unless a new one is clearer engineering.
-
One owner per fact. The map is
docs/README.md. Update the owner file; do not add a parallel spec. Preferdocs/architecture.md,SCHEMA.md, anddocs/crash-recovery.mdover inventing new design notes. -
Prefer high-level functional/integration tests over unit tests (
TESTING.md). A branch no peer, client, or operator can trigger is removed rather than covered by a unit. -
Every PR must keep production line coverage ≥ 91% (
LH*100 >= LF*91). Nightly branch coverage stays ≥90% when measured. Same bar as CI via./scripts/coverage.sh. CRAP--fail-above 30with the allowlist in.cargo-crap.toml. Hold the floor by covering from the surface journey or by deleting unreachable lines, not by adding a private-helper test. -
Target is production server-side node software (wallet backends, etc.). Tip-mode mempool + tx relay are in scope; no pruning/GUI/end-user wallet/ mining without an explicit plan change.
-
Store durability / tip commit:
docs/crash-recovery.mdanddocs/concurrency.md. -
Security-sensitive reports go through
SECURITY.md, not public issues. -
Source-code comments are a smell. A comment that restates what the next statements do means the code itself is not clear — prefer names, types, and structure that read as the algorithm. A comment that restates why those statements exist means the function name or signature is not carrying the contract — prefer a name and type that make the reason obvious at the call site. A comment that explains a weird approach usually means the language, library, or framework is a poor fit — prefer changing the approach or isolating the quirk at a named boundary. Most comments should not exist. Keep a comment only when a specific remaining clarity problem (an invariant, protocol rule, or
SAFETYrequirement the types cannot state) or a specific quirk of why this code exists (a library or workaround constraint that would otherwise look like a bug) still requires it. Crate and public-item rustdoc (//!////) that documents a surface, not a walkthrough of the next line, is not this rule. -
Tests assert behavior, not the repo. Default-suite tests drive a shipped function or scenario and assert an observable result (return, store after reopen, peer/RPC/log line). Do not
include_str!production sources or markdown andcontainsidentifiers, comments, or call graphs. Fixture JSON/hex and tests that read datadir bytes are not this rule. Prefer one true journey over a twin unit for the same reject. A#[test]that only calls other tests is not a journey. A small unit is only for a consensus or schema result a real peer, client, or operator session cannot reach without an absurd chain. Any other behavior no session can trigger is deleted, not unit-tested. Core functional is nightly, not a substitute for that journey. -
RAM and CPU are design inputs. This node indexes chain-scale structures (tens of millions of keys, hundred-MiB arrays, GiB-class heads). Iterating those structures is expensive. Every algorithm should avoid wasting RAM and avoid wasting CPU. Spending one to save the other is allowed only as a named trade — owner doc or rustdoc on the surface, not an accident of the first version that compiled. FdOnly packed
g(mphf_g=0) versus mmap sealed.fuse8(fuse8=0heap) and SH BDZ3 occupancy (mphf_occ=supers only; occ bits arefile=) is that kind of choice. Reclaim of mapped fuse/occ is drop of file pages, not swap.strong_txand mempool stay processVec(Class C tip-last write-behind / live graph); do not mmap those because fuse mapping looked good.Write as if the structure is huge, because in IBD it is. Address it (hash, slot, page, bit offset, subscript). Do not walk everything resident to recover the few fields this call needs. Nested loops over a loaded collection are guilty until the inner work is O(what this call needs), not O(what exists). Tiny fixtures prove correctness, not cost (
TESTING.md). -
Control flow is a type problem. Early return; session state is a struct, not a dozen
mutparameters; dispatch is a catalog of named handlers; boolean products become enums or methods; compose (has-a) instead of a 30-field god struct; one owner per algorithm. A replacement deletes the old branch in the same change (code-shape rule 9). Do not split Core-faithful opcode loops or io_uring machines to beat a line count. Full rules, extract policy, and clippy allows:docs/code-shape.md. Named 0.6.0 extracts (Q-61) are Completed; residual peels:docs/quality.mdR-10. -
Crate visibility is the production graph; the suite reads it through
#[doc(hidden)]. Library tests live intests/suiteand link the normal rlib, so an item a test reaches ispubwith#[doc(hidden)]. Plainpub(no hidden) and crate-rootpub useexist only for names another crate actually imports, or for a documented public library API (we do not have one). If we ever ship an out-of-tree library surface, write the allowlist in the owner crate rustdoc and then unusedpubon that surface is intentional. rustc does not report a dead hidden item, so a change that removes the last caller removes the callee in the same change (code-shape rule 9);#[allow(dead_code)]is not a fix, insrc/or in a suite root. Test hooks in production files are#[cfg(debug_assertions)], and every debug node runs that code, so a hook is either a bounded counter (Cell/ atomic increment) or opt-in: off until the test arms it on its thread. Nothing accumulates unarmed (spindle_markis the pattern). Acfg(debug_assertions)gate never covers an import or re-export that production code uses, and it never changes a production constant or code path (the debug-onlyREDIAL_INTERVAL,SAM_RETRY, andCONSTRAINTS_COOLDOWNvalues are the known exceptions to retire); how CI compiles the release profile is inTESTING.md. Tests drive the shipped function (principle 8), not a second test-only wrapper or*_for_testbackdoor. IO-shape pins prefer session/table instance stats or on-disk file state over a thread-local probe. Fuzz-onlypubis the same smell — prefer the in-crate graph or the published binary surface (rbitcoin-node/ CLI) even if the harness pays a little extra setup, rather than exporting helpers solely forfuzz/.
Unreleased notes are one file per pull in changelog.d/.
Do not edit CHANGELOG.md on a feature branch. The release
cut folds those files in. Owner: docs/releases.md.
Required jobs match .github/workflows/ci.yml
(qc, test, windows, macos, coverage). qc is fmt, ast-grep, deny, the script self-tests,
clippy, then nixos-module-eval, in that order on one runner.
Label nixos-module-runtime to run the NixOS module qemu test (not
eval). That job is not required.
Agents open docs/ORIENT.md when the area is
unclear, follow the agent contract in
docs/how-we-plan.md, and use
.agents/skills/ship-pr/SKILL.md to
open or update a pull request. Humans use ordinary git. The bot host file
does not apply to them.
Humans who want the same gates offline (Nix optional; rustup 1.95 is enough):
# nix develop # Linux and aarch64-darwin — pin via flake.lock; or rustup + rust-toolchain.toml
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$PWD/target/dev}"
cargo fmt --all -- --check # includes nested fuzz/ crate
# rustc warnings are denied via workspace.lints (+ RUSTFLAGS=-Dwarnings in the Nix shell)
cargo build --workspace --all-targets
cargo clippy --workspace --all-targets -- -D warnings
cargo deny check # advisories + licenses (deny.toml)
cargo build -p rbitcoin-node -p rbitcoin-cli
cargo test --workspace
./scripts/coverage.shWindows/macOS contributors matching PR CI: ./scripts/ci-os-smoke.sh (Git Bash
on Windows) rather than the full workspace suite.
Do not treat host or nix-shell cargo build --release as the operator
binary. Musl pin, repro-build.sh / repro-check.sh, and the tag/branch
scripts: docs/reproducible-builds.md,
docs/releases.md. Operator install:
OPERATOR.md. PR windows / macos jobs smoke native store
IO; they do not package zips. GitHub Releases:
.github/workflows/release.yml.
- Small, reviewable commits with complete sentences in the message body.
- Production code and its covering scenarios land together.
- Do not commit live
datadir-*/, operator*.logdumps, secrets, or keys (see.gitignore).
- Behavior covered by a true journey (or a justified unit whose result
is consensus or schema math a real session cannot reach). A behavior
no session can trigger is deleted. No caller-of-tests, no twin
for the same reject. Core functional is not the PR pin
(
TESTING.md). - Core-facing RPC / P2P / Electrum / Esplora:
COMPAT.md - No new silent dead branches
- No unused crate-root
pub/pub use; suite-only items are#[doc(hidden)]; a removed caller removes its callee (principle 11). Tests drive shipped functions, not*_for_testbackdoors. Acfg(debug_assertions)hook is a bounded counter or opt-in, and gates no production import, constant, or code path. Fuzz does not growpubwithout considering the binary surface. - Store changes respect Class A/B/C and allocate-then-publish
- Experimental / milestone honesty preserved in user-facing docs when relevant
- No restating
//comments. Remaining line comments name an invariant, protocol,SAFETYrequirement, or library quirk the types cannot state. - Tests assert shipped behavior, not source/docs text (
include_str!of production.rsor markdown is not a pin). - Hot-path algorithm is O(need) at chain scale, or the RAM/CPU trade is named (principle 9).
- New control flow follows principle 10 /
docs/code-shape.md: early return, session structs, named dispatch, no extra boolean flags.