| 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-formfileby callingrid_from_uuid(&pXfer->aToken[2], 1, isPriv)at line 190 (line 174 on the clone path) withphantomizeset.rid_from_uuid()(lines 70-85) callscontent_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 asnDanglingFile(line 196), andDELETE 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-formcfile(src/xfer.c:287).- The server's end-of-request
request_phantoms()(src/xfer.c:1906) then emitsgimme 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
gimmefor 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:
- 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; replygimme 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.