Peering
DraftPeering connects one user’s own devices directly; server-to-server sharing is Federation. It has two uses:
| Use | What moves | Who ends up holding it |
|---|---|---|
| Sync | A delta of what one device has and the other lacks | Both devices |
| Delegation | Sealed upload bundles a device has not yet uploaded | The delegate, until the server issues custody |
Peering is an accelerator, never a replacement for the server: it moves bytes at LAN speed
and lets devices converge while the server is unreachable, but a copy on another device is
never custody. Only a server custody receipt releases a local original
(invariant 7). Implementation: capsule-sdk::peering.
Payloads
Section titled “Payloads”Peering adds no payload format and no sync engine.
- Sync moves a delta-scoped backup artifact, ingested through the backup restore path.
- Delegation moves upload bundles exactly as the transfer protocol defines them: ciphertext blobs plus the signed manifest, sealed and signed by the originating device before they leave it.
Peering owns only discovery, the channel and the hand-off rules below.
Trust Model
Section titled “Trust Model”Both ends hold device keys cross-signed into the same
device directory. Identity-trusted is not
content-trusted: everything received is re-verified (content hash,
STREAM tags,
verify_asset), and a peer can at worst
withhold or delay, never alter, what it carries.
- Directory anti-rollback. Each device persists the highest
directory_versionit has verified. A peer presenting a lower version is refused, so a stale directory cannot readmit a revoked device. - Revocation is checked on every request, not only at the handshake: a device removed from the directory mid-session is cut off at its next request, and everything it delivered after its revocation took effect is quarantined.
- Each device is its own containment boundary. A manifest behind the local chain head is
quarantined; any
sidecar_schema,crypto_suite_idorprotocol_versionabove the receiver’s maximum is rejected at decode.
Discovery
Section titled “Discovery”Devices advertise over mDNS and accept TCP, LAN-only, with no relay. The advertisement MUST NOT identify the user or device: an opaque instance name, rotated at every boot and at least every 24 h, never a handle, device name or model. Same-user is established inside the channel; no answer means silent fallback to the server. Handshake attempts are rate-limited per source address, so the LAN cannot probe a device for membership.
The limiter tracks a bounded number of source addresses. A new source that finds the table full
of live ones evicts the least recently active source; it is never refused. The limiter keys
on the full address, not a prefix: on a LAN every IPv6 peer shares the link’s /64 (and every
link-local peer shares fe80::/64), so keying by prefix would let one host exhaust the budget of
every peer on the link. The trade-off is that a host holding more addresses than the table
tracks can flush the oldest entries, resetting their budgets. That weakens the rate limit only
for a host that already gets a fresh budget with every new address it uses, and it never locks
a peer out.
The live mDNS responder ships behind the Discovery seam, tested in process.
Establishing the Channel
Section titled “Establishing the Channel”A framed request/response exchange over mutually authenticated TLS 1.3 with no CA. TLS authenticates the device key’s classical half; the hybrid proof that the key chains to the shared User IK (per the signature scheme) is bound to the TLS exporter and verified before any payload byte. Both devices must already be enrolled (Device Enrollment). The proof also covers the sender’s role, dialer or listener, so the two hellos of one session sign different messages and a hello reflected back at its sender never verifies. A hello naming the receiving device’s own id is refused outright: a device never peers with itself.
Every payload byte travels inside that verified TLS session; there is no plaintext path. Each frame’s length is checked against a fixed ceiling before it is read, every read and write is bounded in time, a ranged pull must sustain a minimum transfer rate, and every decision is made against the identity the handshake verified, never against a device id a frame claims. The serving device lists what it offers, then authorizes each request against the directory it pins at that moment, so a peer revoked mid-session is refused at its next request. Peering is LAN-only: a device dials and admits only loopback, private and link-local addresses.
The secret that seals a sync artifact is a 256-bit value from the channel’s TLS exporter under a peering label: fresh per session, never stored, never a user passphrase.
Versioning
Section titled “Versioning”Peering negotiates like the server: each device advertises its supported
[Min, Max] window and the pair speaks the highest
common version. Only disjoint windows end the connection, with a typed error telling the user
which device to update. Artifact formats are versioned by
Backup and Recovery; upload bundles by the
transfer protocol.
- Delta. The receiver opens the session with its offer: the head record hash of every asset it holds, and its sync cursor. The sender cuts its artifact to the complement, honouring the receiver’s synchronization scope, so an asset the receiver holds at the same head is never sent. Offers are padded to a size bucket, capped per page, and capped in pages. The offer is an additive protocol version: a pair that negotiates an older version exchanges none, and the sender sends everything it would have offered, which ingest makes a no-op for each asset the receiver already holds.
- Pull. The device that is behind pulls the artifact with ranged requests over the channel and verifies it against its content address, so a severed transfer resumes on the next channel without re-fetching. A peer may send an advisory hint that carries no authority.
- Ingest through the backup restore path, all-or-nothing per asset: an asset applies
only with its manifest, metadata and every blob its scope requires. A manifest behind the
local chain head is quarantined as “peer sent stale state”. A purged asset is never
resurrected: the device keeps a tombstone for every asset it has purged, its
kept provenance chain, and ingest
refuses any asset it has a tombstone for. A device that cannot read its tombstones refuses
the ingest before it opens a session, never running it against an empty set. A deleted asset in
trash is not a tombstone, since a
trash-restorestill recovers it; its delete manifest is the local chain head, so a peer’s older copy is quarantined as stale. A device learns of a purge from the sync feed, which carries no purge entry: a delete entry carries its signed delete record, and an asset whoseretention_untilhas passed can no longer be restored, so it is purged. The device judges the deadline only once a sync has drained the feed to its head, so a restore on a later page is applied first and takes the asset out of the reckoning. It judges by the server’s clock as the drain saw it, the latestchanged_at, never by its own: a clock running ahead would tombstone the asset early and then refuse a restore the server accepted in time. An asset whose delete is still held (the server has not indexed it) has no window running on the server and is never purged on expiry (Organization — Deletion State Machine). A delete served with no record (after the purge, or from an older server) is no evidence of a purge, and records nothing. - Reconcile. Assets a device learned by peering are not uploaded again if the server holds them; dedup and merge resolve them. An asset no device has uploaded is uploaded by whichever device holds it first, under the rules for delegation below, unless it is in the trash: a device that learns of a delete over peering uploads no create for it, and holds the delete until another upload of the create is indexed.
Delegation
Section titled “Delegation”A device with originals but a poor or metered link (a phone) hands them to a device with a better one (a laptop), which uploads them to the server later. The delegate is an opaque courier: it never holds album keys or plaintext for the delegated assets, and never signs on their behalf.
origin: seal + sign bundle ──hand-off──▶ delegate: persist, upload ──▶ server: receipt ▲ │ └──────────── origin verifies the receipt against its own hash ◀───────┘- Seal. The origin builds the full upload bundle (encrypted original, derivatives, metadata blob, signed manifest) exactly as it would upload it, and records its own content hashes.
- Hand-off. The delegate pulls the bundle over the channel, verifies every hash against the manifest, fsyncs it, and only then acknowledges over the same channel. The acknowledgement is not custody: the origin keeps the original pinned. Over the channel a peer serves only bundles it sealed itself, and the origin takes an acknowledgement only from the device it sealed the bundle for. The bundle is self-verifying, so the same hand-off may also travel by any other courier (the CLI writes it to a file); its integrity never rests on the channel.
- Upload. The delegate uploads the bundle through the normal transfer protocol under its own session. The server accepts a manifest signed by one device of the account and uploaded by another; quota is charged to the account as usual. The delegate never re-signs, re-encrypts or re-derives anything.
- Release. The origin releases an original only when the server returns a custody receipt for the content hash the origin computed itself, and a read-back verify agrees. A receipt fetched through the delegate is not accepted.
- Cleanup. The delegate deletes its copy after it holds the receipt. A delegate copy is never visible as a library asset on the delegate.
Edge cases:
| Case | Rule |
|---|---|
| Delegate dies, loses the bundle or withholds it | The origin sets a monotonic deadline at hand-off. On expiry, or when the delegate is revoked, it re-queues the upload itself or delegates again, unless the asset is in the trash: no asset is uploaded only so that its delete can follow. The hand-off is not withdrawn, and a restore returns the asset to the queue. |
| Both devices upload | Dedup resolves it; the receipt names the content hash, so either upload’s receipt releases the original. |
| Origin deletes the asset after hand-off | The origin holds the signed delete locally until the create’s receipt exists, then sends it; the server never sees a delete for an asset it has not indexed. The delegate does not drop its copy on learning of the delete: it never deletes a copy the server has not taken custody of, and the held delete supersedes the create once it lands. |
| Delegate uploads after the origin purged | The held delete outlives the purge in the origin’s tombstone, headed by the shorter-retention re-issue the purge appended, and is sent from it once the create is indexed; the server then purges by that window. |
| Origin edits metadata after hand-off | The edit is a new manifest the origin uploads itself; the delegated create stays valid. |
| Partial hand-off | Per asset, all or nothing: a delegate never uploads an incomplete bundle. |
| Delegate tampers with bytes | Hashes and the manifest signature fail at the server and at the origin’s verify. |
| Delegate on an older protocol | It must speak the bundle’s protocol_version; otherwise it refuses the hand-off and the origin keeps the upload. |
| Held hand-off permanently refused | The delegate never deletes a copy the server has not taken custody of. When a held hand-off can never succeed it is quarantined: the copy is kept byte for byte, no later run retries it, the delegate reports it once with a stable reason, and only the user deletes it. A revoked origin is often a lost device, so the delegate’s copy may be the only one left. Never succeeds: the origin is revoked or gone from the directory after acceptance; the held bytes no longer verify; or the server refuses with error.upload.envelope_rejected, envelope_mismatch or stale_revival (a conflicting provenance), or at creation with timestamp_out_of_range, album_access_denied or device_not_authorized, which are the same conditions met one step earlier. envelope_rejected covers a manifest under a stale album epoch, a signer that is revoked, of another account or not verifying, a role conflict, a timestamp out of range, the album closed or the uploader’s write access withdrawn since creation, and a signing device no longer authorized. The delegate cannot change a request the origin signed, so retrying cannot help. Any other failure (network, outage, quota, rate limit, this device’s own directory state, a newer format) keeps the copy for the next run. Separately, the origin keeps its own original pinned and re-queues the upload at its deadline, unless the asset is in the trash. |
| Delegate compromised | It learns what the server learns (ciphertext, sizes, timing), plus the origin’s hand-off schedule. It cannot read or forge assets. |
The delegate follows the same battery, metering and scheduling policy as its own uploads (Sync — scheduling). The origin shows each delegated asset as “handed to {device}, awaiting server” until the receipt arrives.
Robustness
Section titled “Robustness”Ranged transfers resume; a vanished peer equals no peer; reordering a transfer cannot change state (no order trust). Deadlines use monotonic time, so clock skew between devices changes nothing.
Validation
Section titled “Validation”- Advertisement (unit): no handle, device name or model; rotates per boot and per 24 h.
- Handshake (unit): same-IK succeeds; revoked device, foreign IK and a rolled-back directory fail; handshake rate limit holds; a table full of live sources, even one prefix’s worth, still admits a new peer.
- Version window (unit): overlapping windows pick the highest common version; disjoint windows refuse with the typed error.
- Delta (unit): complement of two address sets; offers are padded and capped.
- Offer exchange (smoke): a receiver’s offer crosses the channel whole before the listing; a receiver holding every head is sent an artifact with no assets; a receiver that sends no offer, or an older peer, is sent everything.
- Ingest (smoke): an artifact restores fully; a partial asset is not applied; a tombstoned asset is not resurrected; a stale chain head quarantines.
- Revocation mid-session (smoke): a peer revoked during a transfer is cut off at its next request and its later deliveries are quarantined.
- Resume (smoke): a severed transfer resumes without re-fetching.
- Delegation (E2E): origin hands off, delegate uploads, origin releases only on its own receipt check; plus each edge case in the table above, including delegate death, double upload, delete-after-hand-off and tampering.
- LAN sync then server reconcile (E2E): one case in Module Map.