| Document | draft-fossil-sync-protocol-00 |
| Section | 8.6 — Announcement and Request Cards; 3.7 — Loop Termination, limb (c) |
| Type | Technical (correction — the draft attaches the peer-date gate and the extra round to the wrong private igot) |
| Status | Proposed — operator decision pending |
| Reported | 2026-10-11 |
| Submitter | Dan Mestas |
| Found via | go-libfossil's conformance suite against Fossil 2.28 (GAP rows SP-3.7-3 and SP-8.6-1); confirmed by reading Fossil's source and by running a Fossil client against a scripted peer |
1. Summary
Fossil sends igot HASH 1 from two different places, and the draft merges
them:
- Private-sync inventory. With private sync on, the client lists every
private artifact as
igot HASH 1in every push-carrying request, starting with the first. There is no peer-date check, nothing is held untilserver-versionarrives, and these cards never cause another round. - Private answer to a gimme. With private sync off, a side asked for an
artifact it holds as private answers with
igot HASH 1instead of the content, but only when the peer's date is 2020-04-13 or later. On the client, an answer appended while processing the first reply is what buys the one extra round.
§8.6 applies the date gate and the hold-until-server-version rule to (1). §3.7(c) ties the extra round to (1) as well. Fossil does neither.
2. Location
§8.6 (~lines 1751-1760):
For peer dates before 2020-04-13, omit private igot entirely rather than sending flag 1. Until server-version establishes the peer date, hold local private inventory; if the reply establishes a date on or after 2020-04-13, append the eligible private igot cards to that reply's next-request buffer, and let Section 3.7(c) dispatch that buffer.
§3.7(c) (~lines 588-594):
The current request emitted at least one private igot HASH 1 and the post-increment round number is exactly 1, or processing server-version in this reply made private inventory eligible and the post-reply remaining-inventory step appended at least one such igot. …
3. The divergence
Source, Fossil 2.28 (src/xfer.c):
send_private()(lines 978-991) emitsigot %s 1for every row ofprivateand has noremoteDatetest. The client calls it in every push request when private sync is on (line 2295), including the first, before anyserver-versionhas been read (remoteDateis set only at line 2811). Its return value is discarded, so it never touchesnPrivIGot. The server calls it the same way in pull and clone replies (lines 1920, 1924).send_file()(lines 543-560) is the only place that incrementsnPrivIGot(line 557). For a private artifact while private sync is off, it emitsigot HASH 1only ifpXfer->remoteDate>=20200413(line 549); the comment there gives the reason: older peers raise an error on a private igot when private syncing is disallowed. On the client this runs while answering a reply'sgimme(lines 2564-2569).nPrivIGotis zeroed right after each exchange (line 2456), so it counts only what reply processing appended to the next request. The continuation test isxfer.nPrivIGot>0 && nCycle==1(line 2989, afternCycle++).
Runs, Fossil 2.28 client with one private check-in, against a scripted peer:
| Command | Peer's first reply | Observed |
|---|---|---|
fossil sync --private |
server-version … 20260311 …, nothing else |
request 1 carries both igot HASH 1 cards; 1 round |
fossil sync --private |
server-version 21000 20190101 000000, igot NEW |
request 1 and request 2 both carry the private igots (peer dated 2019) |
fossil sync |
server-version … 20260311 …, gimme PRIVHASH |
request 2 carries igot PRIVHASH 1; 2 rounds |
fossil sync |
server-version 21000 20190101 000000, gimme PRIVHASH |
no private igot; 1 round |
4. Impact
- Wire. A client following §8.6 sends its private inventory one round later than Fossil and withholds it from pre-2020 peers that Fossil sends it to. A client following §3.7(c) goes one extra round after a first private-sync request where Fossil stops, and does not go one where Fossil does.
- Interop risk of Fossil's way. Low: a peer accepting
pragma send-privateunderstandsigot HASH 1. The date gate guards the other case, where private sync is off. - Conformance. Neither rule as drafted can be observed in Fossil, so go-libfossil has no case for either.
5. Correction
- igot HASH announces public possession; igot HASH 1 private
- possession; gimme HASH requests content. Section 3.3 governs. No
- ordering constraint exists between igot and gimme. For peer dates
- before 2020-04-13, omit private igot entirely rather than sending
- flag 1. Until server-version establishes the peer date, hold local
- private inventory; if the reply establishes a date on or after
- 2020-04-13, append the eligible private igot cards to that reply's
- next-request buffer, and let Section 3.7(c) dispatch that buffer.
+ igot HASH announces public possession; igot HASH 1 private
+ possession; gimme HASH requests content. Section 3.3 governs. No
+ ordering constraint exists between igot and gimme. When private
+ sync is in effect, a client lists its private inventory as igot
+ HASH 1 in every request that carries push, starting with the first,
+ and a server lists its own in every pull or clone reply; no peer
+ date is consulted. When private sync is not in effect, a
+ gimme for content held as private is answered with igot HASH 1 only
+ if the peer's date is 2020-04-13 or later; for an earlier or unknown
+ date, send nothing for it.
- *(c)* The current request emitted at least one private igot HASH 1
- and the post-increment round number is exactly 1, *or* processing
- server-version in this reply made private inventory eligible and the
- post-reply remaining-inventory step appended at least one such igot.
- The latter continuation dispatches the newly appended first private
- inventory exactly once; the former permits only one first-cycle
- discovery continuation.
+ *(c)* The post-increment round number is exactly 1 and processing
+ this reply appended at least one igot HASH 1 answering a gimme
+ (Section 8.6, private sync not in effect). Private-inventory igot
+ cards sent under private sync never satisfy this condition.
6. Rationale
- The date gate belongs where Fossil put it. It protects old peers when
private sync is off. A peer in a private sync has accepted
send-privateand already knows the flagged igot. - No hold, no extra round. Fossil has nothing that holds inventory until
server-version, and the second limb of (c) describes that missing mechanism. Removing it makes the loop rule one condition Fossil actually evaluates. - Both limbs become testable against Fossil, which the current text is not.
7. Verification
- Source: Fossil 2.28
src/xfer.c:543-560(send_file, gate at:549, count at:557),:978-991(send_private),:2295,:2456,:2564-2569,:2807-2813,:2989,:1920,:1924. - Not run: a pre-2020 Fossil peer. Whether such a peer accepts the private
igots of a private sync was not tested; the low-risk claim in §4 rests on
its having accepted
send-private. - Run: the four client runs in §3, each logging decoded request bodies at a scripted HTTP peer.
Prepared against draft-fossil-sync-protocol-00. The RFC remains the oracle;
the reference implementation is used only to confirm the observable behavior.