# The `RECON` payload

`/protocol/relay/wire.md` names the `RECON` frame and specifies the algorithm
family ("Negentropy over the per-group sequence space") without fixing the bytes.
Step 5 fixes them, so they are written here rather than left implicit in two
implementations.

Both implementations are held to
`/protocol/contracts/wire/vectors/recon.json`, which is generated by
`scripts/fixture-log.sh --recon-vectors` from `Sources/Sync` and reproduced by
`backend/wealdrelay/tests/recon_vectors.rs`. A vector corpus generated by the side
under test would prove only that a side agrees with itself.

## Shape

Deterministic CBOR, the same subset every other frame uses
(`Sources/Sync/DeterministicCBOR.swift`, `backend/wealdrelay/src/cbor.rs`).

```text
Message = [version: uint, ranges: [Range]]
Range   = [upper: uint, mode: uint, payload]
  mode 0  skip         payload = null       settled, nothing to do
  mode 1  fingerprint  payload = bytes(32)  BLAKE3 over the items in the range
  mode 2  idlist       payload = [bytes(32)] every id in the range, ascending
```

`version` is 1 and is separate from the frame set's protocol version: the frame set
and this payload can move independently, and a client speaking frame version 1 with
a payload the relay does not know is one to refuse precisely rather than vaguely.

## Rules a receiver enforces

Each is refused rather than interpreted, and each maps to a code in
`registries/error-codes.md`: a CBOR failure is `reject/noncanonical_cbor` and
everything else here is `reject/malformed_header`.

- **The cover is complete.** The first range starts at zero, each covers
  `[previous upper, upper)`, and the last range's `upper` is `2^64 - 1`. A cover
  that stops short would be read as "everything above this is settled", and the
  symptom would be one member missing envelopes nobody else is missing.
- **Upper bounds strictly ascend.** Two ranges with one bound would leave the two
  sides disagreeing about which range covers an item.
- **Ids within a range strictly ascend.** One set therefore has one encoding, so
  two implementations cannot disagree about the fingerprint of what they hold.
- **Bounds on size.** At most 1024 ranges per message and 4096 ids per range. The
  frame size limit alone does not bound the work, because a skip range is three
  bytes and a peer could otherwise make the relay do a million index scans for one
  frame.

## The fingerprint

```text
BLAKE3("weald negentropy fingerprint v1" || u64be(count) || (u64be(seq) || id)*)
```

The count is inside the preimage because a hash over concatenated ids alone would
let two different sets collide by construction. `seq` is inside it because an
envelope the relay renumbered must not fingerprint identically: a silent
renumbering is a server-side rewrite the client is entitled to notice.

## Roles

The client drives and the relay answers. Both sides run the same comparison, which
is why both are pinned by the vectors.

| Incoming | Answer |
| --- | --- |
| `skip` | `skip`. A side that has said a range is settled is not argued with; re-examining it would make the exchange non-terminating whenever a write landed mid-round. |
| `fingerprint`, equal | `skip`. |
| `fingerprint`, differing, at most 16 items locally | `idlist` of the local ids. |
| `fingerprint`, differing, more | split into at most 8 sub-ranges, each fingerprinted, at item positions rather than by dividing the numeric space. |
| `idlist` | push what the peer lacks; answer `skip` if the peer holds nothing extra, and `idlist` otherwise so the peer learns what to resend. |

Sixteen and eight are chosen against what they cost: an id is 32 bytes, so a full
id list is 512 bytes and beats the round trip that splitting would spend, while
above it eight fingerprints are 256 bytes and divide the differing region by eight.

## Two ordering rules that are part of the protocol

- **The relay sends pushes before its answering `RECON`.** The client's next round
  is computed against what it holds, so a client that received the answer first
  would ask again for envelopes already in flight. Frames on one WebSocket are
  ordered, so this is a guarantee rather than a race.
- **The client applies its `SEND_ACK` sequence numbers before it answers.** An
  envelope held without a relay sequence number is not in this space until the
  relay numbers it. A client that answers from its pre-`SEND` view is safe, because
  the hash is a content address and it already holds the envelopes, but it spends a
  round being pushed them back. Asserted by
  `a_client_that_answers_before_its_acks_is_safe_but_wasteful` in
  `backend/wealdrelay/tests/negentropy.rs`.

## What this space is not

An order. `wire.md` requires that causality comes from the Automerge change graph
and never from `seq`, and nothing here or above it concludes that a lower `seq`
happened first: the sequence space is a search index over a set. Envelopes that
arrived by git have no relay `seq` at all and do not take part in the exchange;
the client `SEND`s them, and a duplicate `SEND` is answered with the existing
sequence number, so that path costs nothing and needs no protocol of its own.
