Files
ngit-grasp/tests
DanConwayDev 629be17e64 fix(git): stage unsigned uploads until a signed event accepts them
A push to refs/nostr/<event-id> is accepted before its PR event is known,
and its objects went straight into the identifier family. The family is
never garbage-collected, so anyone could fill permanent storage without
signing anything, and an upload whose event never arrived stayed forever.

Approach

Route by what push authorization already decides. A ref named by a signed
State or PR event, accepted or in purgatory, is received into the family as
before. A refs/nostr ref with no event, or only a placeholder, is received
into the view's own object directory. Pre-validation now reports whether a
match was signed, because a placeholder match was indistinguishable from a
stored event.

Accepting a PR event first promotes its tip: fetch the history from the
view into the family, check the family alone holds it down to existing
retained roots, then install the usual retained and base roots. On failure
the event is rejected and its placeholder kept, so the upload expires
normally and the event can be sent again.

While a view holds staged objects every push to it is staged, because the
view advertises pending refs and a client may omit objects only staging
holds. Signed tips of such a push are recorded as owed before Git runs and
promoted when it finishes. Compaction refuses to run while anything is
owed: rollback after a State deletion needs history no ref names, so a
missing ref never proves history is disposable. Objects a view holds before
it is first staged are moved into the family.

Staging is reclaimed with git repack -a -d -l and git prune. Git can
install a ref whose parent a concurrent repack removed (see
tests/git_cruft_concurrency.rs), so compaction takes the family write lease
that every push already holds. It waits at most 250ms and retries with
backoff. One worker handles requests from pushes, promotions and ref
deletions, and reads the persistent registry at startup.

The /prs/ handler now releases the family lease before post-push
processing, as the standard handler does. Promotion of a waiting PR event
re-enters the family and would otherwise deadlock.

Assumptions

- Views and their family are on one filesystem; moving pre-existing objects
  uses hard links.
- Retained roots are complete. A damaged root fails promotion and is left
  to the integrity pass.
- One server process per storage root, as the family lease already assumes.

Excluded

- Storage quotas. Staging bounds how long an unsigned upload is kept, not
  its size.
- A pack from a signed push is stored whole. A signer can make any object
  reachable from their own tip, so filtering it would protect nothing.
- Fetches take no lease. A fetch of a pending ref that expires while being
  served may fail.
- Archives store a view as it is; restoring one imports staged objects.
- State acceptance, rollback and purgatory sync are unchanged.

Validation

- nix develop -c cargo test --lib git::staging: 13 passed, covering
  reclamation, a pending sibling keeping its history, promotion, refusal of
  incomplete history, owed history surviving ref deletion, restart recovery
  and a busy family.
- nix develop -c cargo test --test pending_upload_staging: 4 end-to-end
  tests passed, including an unsigned upload across a relay crash and a
  signed push that omits objects only staging holds.
- cargo clippy --workspace --all-targets -- -D warnings and cargo fmt
  --check were clean.

Assisted-by: Claude Fable 5.1
2026-09-29 10:21:41 +00:00
..