Zero vendors seven upstreams as git subtrees (see SUBTREES.md for the mechanics). This file is the policy: how we make changes so that two flows stay cheap forever.
- Upstream useful fixes back to zcash/zebra/zaino/zallet/librustzcash/lightwalletd.
- Pull useful updates down from those upstreams.
Both costs scale with how far our tree diverges from upstream. So the entire discipline reduces to one rule: keep our intentional delta small, explicit, and classified.
Every change is one of two kinds. Decide up front.
Work upstream-first: make the change in a clone of the real upstream repo,
open a normal PR there, and let it flow home via git subtree pull. Do not
originate it in Zero.
If we need the fix in Zero before it merges upstream, cherry-pick the commit
in-tree as a temporary carry, marked [upstream-pending #<PR>], and drop it
on the next subtree pull once it lands.
Use the upstream-change skill to prepare the PR branch from an existing in-tree
commit when a change started life in Zero by accident.
Commit directly in-tree, prefixed [zero]. Examples:
- zcashd Ironwood support + the hardcoded end-of-life logic.
- Enterprise packaging, config defaults, deployment glue.
- Z3 integration wiring across components.
- Branding / Shielded Labs specifics.
The commit is the record. Put the rationale (and any non-default conflict
guidance) in the commit body. There is no separate ledger to keep in sync: the
delta is whatever git log --grep returns (see below).
The rule is not uniform. Bias differs by how alive the upstream is.
| Component | Upstream state | Default bias |
|---|---|---|
zebra |
Active (ZF) | Upstream-first, strongly |
zaino |
Active (Zingo) | Upstream-first, strongly |
zallet |
Active (ZODL) | Upstream-first, strongly |
orchard |
Active (ZODL); Ironwood released on main |
Upstream-first, strongly |
librustzcash |
Active (ZODL) | Upstream-first, strongly |
lightwalletd |
Active (ZODL), low churn | Upstream-first for fixes; PIR experiments start Zero-only |
zcashd |
Winding down; we hardcode EOL | Mostly Zero-only; upstream only clear bugfixes |
A subject has two independent parts: an optional divergence marker then a conventional-commit type.
The one question that decides the marker: does the commit touch a vendored
subtree dir (zcashd/, zebra/, zaino/, zallet/, orchard/,
librustzcash/, lightwalletd/)?
- Yes -> lead with a marker, then the type:
[zero] <type>: ...- permanent Zero-only divergence.[upstream-pending #N] <type>: ...- temporary carry of an unmerged upstream PR; dropped on the next subtree pull after #N merges.
- No (our own files: README, SUBTREES.md, MAINTENANCE.md,
.claude/) -> just the type, no marker.
The type is the usual feat / fix / doc / skill / chore / etc.
Examples:
| Commit | Touches vendored dir? | Subject |
|---|---|---|
Security contact in zebra/SECURITY.md |
yes, permanent | [zero] fix: route security reporting to Shielded Labs |
| Stopgap fix awaiting upstream PR | yes, temporary | [upstream-pending #42] fix: ... |
| New maintenance doc | no | doc: ... |
| New skill | no | skill: ... |
| Merge commits (subtree, PR, sync) | yes, auto | (git's own message, no marker; if the merge resolved conflicts, note the resolution in the body) |
The git log is the ledger, queryable in both directions:
git log --grep='^\[zero\]' -- zcashd zebra zaino zallet orchard librustzcash lightwalletd- our permanent delta. Add--statfor files, or narrow the pathspec to one dir/file.git log --grep='upstream-pending' -- zcashd zebra zaino zallet orchard librustzcash lightwalletd- our outstanding carries.
The vendored pathspec (-- zcashd zebra zaino zallet orchard librustzcash lightwalletd) is what makes this
authoritative, not the prefix alone. The real delta is "commits that both carry
the marker and touch a vendored dir," so a stray marker on a root-file commit
(e.g. a mislabeled .gitignore change) drops out automatically. The query stays
pristine without anyone having to police prefixes or rewrite history.
Merge commits (subtree imports and pulls, PR merges, sync merges of
origin/main) never take a marker: they join histories rather than introduce
delta, and the ledger queries are unaffected because git log traverses both
parents, finding the marked commits inside the merged branch. The one hazard is
a merge that resolves conflicts, since the resolution is real content living in
an unmarked commit the ledger cannot see (pathspec'd git log even simplifies
merges away unless given --full-history). Keep merges content-free where
possible; when a merge must resolve conflicts against the [zero] delta, apply
the "ours wins" default below and state the resolution in the merge body.
There is no separate file to maintain. Write a clear commit body and the delta documents itself.
When a subtree pull conflicts on a line we changed in a [zero] commit, ours
wins by default: keep the Zero change, keep the rest of the upstream update.
A commit may override this in its body if a particular change should yield to
upstream. To recover the rationale during a conflict, grep the change:
git log --grep='^\[zero\]' -- <conflicted-path>.
Comments in vendored code are part of the [zero] delta: upstream maintainers,
reviewers, and generated docs all read them. Keep them minimal.
- Say only what the code cannot: the why, the upstream reference, the invariant. Never narrate the next line, and never restate the commit message or a review thread in the code.
- No agent-isms: no
// @claudetags, no review-round references such as(review H3), no "Note that", "Importantly", "This ensures". A comment must read as if the upstream maintainer wrote it. - Default to a plain line comment (
//in Rust, C++, and Go). Doc comments (///,//!,/** */) are API documentation and render in generated docs, so they never carry vendor notes. - Mark a Zero divergence with one
// [zero]line at the top of the block, as inzebra/zebra-state/src/service/check/difficulty.rs. The comment carries one line of why; the commit body carries the rest (see "Commit-message convention").
- Don't track moving branches forever. Pin to upstream release tags when
one exists; pull
main/dev/masteronly deliberately. - Pull one component at a time, on its own branch, with tests, never blind on
main. - Use the
update-subtreeskill: it fetches, summarizes incoming commits, pulls--squashon a branch, surfaces conflicts with our[zero]delta for review, and runs that component's test suite. - The weekly upstream-watch scheduled job reports what is new and the conflict risk against our delta. It never merges on its own.
Every Rust workspace carries a cargo vet
store (<component>/supply-chain/; zcashd keeps its at zcashd/qa/supply-chain
via [package.metadata.vet]). CI (cargo-vet.yml, check "cargo vet result")
fails any PR whose dependency graph contains a third-party crate version that is
neither audited (ours or imported from Mozilla, Google, Bytecode Alliance, ISRG,
Embark, Fermyon, or ECC's rust-ecosystem) nor exempted in that store. The point
is not that every crate is audited today (most are baseline exemptions); it is
that a dependency can no longer change without a reviewable diff under
supply-chain/.
When the gate goes red after a dependency change, in the component directory:
cargo vet(network) refreshesimports.lock; a bumped crate is often already covered by a newer imported audit.- For what remains,
cargo vet diff <crate> <old> <new>to actually review the delta andcargo vet certify, orcargo vet regenerate exemptionsto accept the current graph as baseline. Regenerating is an explicit trust decision: say so in the PR rather than burying it. - Commit store changes with the component's normal
[zero]prefix; a subtree pull that brings upstream's own store update keeps the plain subtree commit.
Divergence to keep: zebra's upstream config marks its own crates
audit-as-crates-io = true (they publish them and exempt each release). Our
fork deliberately diverges from the crates.io releases, so we set false:
first-party code is reviewed through our PR process, not audited as a registry
copy. Without this, every zebra version bump demands fresh exemptions for
zebra's own crates. Upstream zebra does not enforce its store in CI, so expect
it stale on pulls; zallet and librustzcash enforce theirs (audits.yml) and
should arrive green.
CHANGELOG.md at the repo root carries one ## vN section per release:
grouped one-liners (Security / Fixed / Performance / Deploy / Testing / CI /
Docs as applicable), succinct and complete, with commit SHAs. release.yml
refuses to release unless a non-empty section for the version exists, and
embeds it in the GitHub release body. Stage entries under ## Unreleased as
work lands; cutting a release means retitling that section to
## vN - YYYY-MM-DD, pushing, then dispatching the workflow.
Both node forks halt on mainnet at a hardcoded height that only moves when someone edits it: v20 through v27 all shipped v20's zcashd value and shared one halt block (3,471,448, ~2026-09-04), noticed two days ahead. Before retitling the changelog:
- zcashd. Set
APPROX_RELEASE_HEIGHTinzcashd/src/deprecation.hto the current mainnet height plus a ship-day buffer (about 1,150 blocks per day). The halt is 7 weeks of blocks (56,448) later; put that height and its approximate date in the changelog entry, as v28 does. The same constant seeds the defaultsignrawtransactionbranch id, so never set it past a pending upgrade's activation height. - zebra. The halt is
ESTIMATED_RELEASE_HEIGHTplusEOS_PANIC_AFTERdays of blocks inzebra/zebrad/src/components/sync/end_of_support.rs. Zero carries that height as a[zero]line pinned to the newest upstream zebra release's value: re-set it (or pull that tag), then verify the resulting halt is comfortably after the next planned release. release.ymlrefuses to dispatch when either halt is under 4 weeks past the mainnet tip (estimated from a fixed block anchor and the wall clock, no network access). That is a floor, not a substitute for steps 1 and 2.
- Use the
upstream-changeskill: it splits the relevant prefix history, rebases onto fresh upstream, reformats commits to that project's CONTRIBUTING style, runs their tests, and drafts the PR. - Keep PRs focused and atomic. One concern per PR. Never bundle Zero glue into an upstream PR.
- When a PR merges, remove any matching
[upstream-pending]carry on the next subtree pull.
The historically miserable parts of fork maintenance are exactly the mechanical
ones agents handle well: 3-way conflict resolution informed by the [zero]
commit that introduced the change, splitting/rebasing/reformatting commits into
clean upstream PRs, and triaging large batches of incoming upstream commits.
Humans review; agents do the surgery.