Skip to content
zit Artifact [e1a254fd75]
Login

Artifact [e1a254fd75]

Artifact e1a254fd751c19b385beddb209fe11d6996832299263518ed6ebb5f83a11291c:


# Erratum — draft-fossil-sync-protocol-00 §7.4

| | |
|---|---|
| **Document** | `draft-fossil-sync-protocol-00` |
| **Section** | 7.4 — Delta Sources and Phantom Creation |
| **Type** | Technical (correction — the draft forbids a side effect that Fossil always produces) |
| **Status** | Applied in `draft-fossil-sync-protocol-01` (2026-10-12) |
| **Reported** | 2026-10-11 |
| **Submitter** | Dan Mestas |
| **Found via** | go-libfossil's conformance suite against Fossil 2.28 (scope note SP-7.4-1, cases `sync.7.4.verified-duplicate` and `sync.7.4.verified-duplicate-server`); confirmed by reading Fossil's source and by pushing it a duplicate delta |

## 1. Summary

§7.4 says a receiver MUST first check whether a delta card's HASH is already
verified locally and, if so, MUST NOT create a DELTASRC phantom. Fossil does
the opposite order: it resolves DELTASRC, creating a phantom when it is
absent, before the storage call notices that HASH is a duplicate. The verified
target is left untouched, as the draft says, but the source phantom stays, and
the receiver then asks the peer for it with `gimme`.

The 29 July copy of the draft described Fossil correctly here ("Fossil
resolves or creates the DELTASRC record before the storage operation can
detect that HASH already names a locally verified artifact"). zit's draft
replaced that paragraph with the check-first MUST, which every Fossil peer
violates.

## 2. Location

§7.4, first paragraph (~lines 1313-1317):

> Before delta-source handling, a receiver MUST check whether HASH
> already names a locally verified artifact.  If so, it consumes and
> ignores the duplicate card's payload; it MUST NOT replace or
> downgrade the verified target, create a DELTASRC phantom, or enqueue
> an edge.

## 3. The divergence

Source, Fossil 2.28:

- `xfer_accept_file()` (`src/xfer.c:134`) handles a delta-form `file` by
  calling `rid_from_uuid(&pXfer->aToken[2], 1, isPriv)` at line 190 (line 174
  on the clone path) with `phantomize` set. `rid_from_uuid()` (lines 70-85)
  calls `content_new()` at line 83 when DELTASRC is absent, which makes the
  phantom. Nothing looks HASH up first.
- With the new phantom, `content_get(srcid, …)` fails (line 191), so the card
  takes the dangling-delta branch: `content_put_ex()` is called, counted as
  `nDanglingFile` (line 196), and `DELETE FROM phantom WHERE rid=…` runs
  (line 197).
- `content_put_ex()` (`src/content.c:558-566`) finds HASH already stored with
  a non-negative size and returns its rid without writing anything. The target
  is neither replaced nor downgraded; that part of the draft holds.
- `xfer_accept_compressed_file()` does the same for a delta-form `cfile`
  (`src/xfer.c:287`).
- The server's end-of-request `request_phantoms()` (`src/xfer.c:1906`) then
  emits `gimme DELTASRC`. On the client, the dangling count keeps the loop
  going while the phantom table is non-empty, and the next request asks for
  DELTASRC.

Run: a `fossil new` repository, `nobody` given `gioz`. Its initial check-in
manifest A (`7ea4a5db…`) is stored. A delta from an unrelated, absent text U
(`01256859…`) to A was built with `fossil test-delta-create` and pushed as
`push 0000 PROJECTCODE` / `file A U SIZE` through `fossil http` as
`application/x-fossil-debug`. The reply contains `gimme 01256859…` with
`errors 0`; afterwards `blob` holds U with size -1 and `phantom` holds its
rid. A is unchanged.

## 4. Impact

Small and self-healing.

- **Wire.** A Fossil receiver sends one `gimme` for a source it does not need.
  The sender normally holds it, so it arrives and the phantom resolves; the
  cost is one artifact and possibly one round.
- **Conformance.** As written, the MUST NOT makes every Fossil peer
  non-conforming and a conformance test cannot check it against Fossil (which
  is why go-libfossil's case leaves the phantom out).
- **Residue.** If the sender cannot supply DELTASRC (shunned, private without
  private sync), the phantom remains, like any other unfulfilled phantom.

## 5. Correction

Restore the 29 July description of Fossil, and keep check-first as a
permission rather than a requirement:

```diff
-   Before delta-source handling, a receiver MUST check whether HASH
-   already names a locally verified artifact.  If so, it consumes and
-   ignores the duplicate card's payload; it MUST NOT replace or
-   downgrade the verified target, create a DELTASRC phantom, or enqueue
-   an edge.
+   If HASH already names a locally verified artifact, the receiver
+   consumes and ignores the duplicate card's payload and MUST NOT
+   replace or downgrade the verified target.  For a delta-form file or
+   cfile, Fossil resolves or creates the DELTASRC record before its
+   storage operation detects the duplicate, so an absent DELTASRC is
+   left behind as a phantom and is requested by a later gimme; the
+   card counts as a dangling delta for the content-received flag of
+   Section 3.7.  A
+   receiver MAY check HASH first and skip delta-source handling; peers
+   MUST tolerate either behavior.
```

## 6. Rationale

- **The draft should describe the reference, not forbid it.** The difference
  costs at most one extra artifact transfer and never corrupts state, which is
  not enough to make Fossil non-conforming.
- **The useful guarantee survives.** "Do not replace or downgrade the verified
  target" is what protects data, and Fossil keeps it.
- **Check-first stays available.** A cleanroom receiver that avoids the extra
  phantom is still conforming under MAY.

## 7. Verification

- Source: Fossil 2.28 `src/xfer.c:70-85` (`rid_from_uuid`), `:174`, `:190`,
  `:191-197`, `:287`, `:1906`; `src/content.c:558-566`.
- Run: the duplicate-delta push in §3 against a fresh repository through
  `fossil http`; reply `gimme 01256859…`, phantom row present, target intact.
- The 29 July copy (`~/fossil-rfc/draft-fossil-sync-protocol-00.txt`, §7.4,
  ~line 1312) carries the paragraph the correction restores.

---

*Prepared against `draft-fossil-sync-protocol-00`. The RFC remains the oracle;
the reference implementation is used only to confirm the observable behavior.*