# Erratum — draft-fossil-sync-protocol-00 §8.6 / §3.7(c)
| | |
|---|---|
| **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** | 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 (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:
1. **Private-sync inventory.** With private sync on, the client lists every
private artifact as `igot HASH 1` in every push-carrying request, starting
with the first. There is no peer-date check, nothing is held until
`server-version` arrives, and these cards never cause another round.
2. **Private answer to a gimme.** With private sync off, a side asked for an
artifact it holds as private answers with `igot HASH 1` instead 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) emits `igot %s 1` for every row of
`private` and has no `remoteDate` test. The client calls it in every push
request when private sync is on (line 2295), including the first, before
any `server-version` has been read (`remoteDate` is set only at line 2811).
Its return value is discarded, so it never touches `nPrivIGot`. The server
calls it the same way in every pull reply (line 1924); line 1920 is the
legacy clone path, which clone version 3 does not take.
- `send_file()` (lines 543-560) is the only place that increments `nPrivIGot`
(line 557). For a private artifact while private sync is off, it emits
`igot HASH 1` only if `pXfer->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's `gimme` (lines 2564-2569).
- `nPrivIGot` is zeroed right after each exchange (line 2456), so it counts
only what reply processing appended to the next request. The continuation
test is `xfer.nPrivIGot>0 && nCycle==1` (line 2989, after `nCycle++`).
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-private` understands `igot 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
```diff
- 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 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.
```
```diff
- *(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-private`
and 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.*