ecgang 874924caf8 Courier spray: restore the budget when a taker deterministically declines
Upstream's send-gated `transferSprayCopies` already closes the
link-drop-before-send loss case: it only decrements copies, inserts the
courier into `sprayedTo`, and persists after the local BLE send is
accepted. But courier writes are `.withoutResponse`, so send-accepted is
not taker-received: the spend still stands when the taker deterministically
refuses the deposit (bad/missing signature, unknown peer, policy nil,
quota full, invalid), and those copies leave the network unretryable.

This adds the orthogonal second half on top of the send-gated commit: a
capability-gated, signed receipt that gives the budget back only when the
taker deterministically declines.

- Wire (additive, ignored by old peers): courierSprayAck 0x2A ("stored it",
  retire the offer early) and courierSprayDecline 0x2B ("deterministically
  refused" -> restore). New capability bit PeerCapabilities.courierAck
  (1 << 8), advertised in localSupported; givers branch on the taker's
  advertised capability, so old takers keep the unchanged path.
- Receipt payload is the envelope's ciphertext hash (existing dedup key),
  so no CourierEnvelope wire change.
- Giver: offerSprayCopies is transferSprayCopies verbatim plus one
  in-memory pendingSprayOffers restore record in the commit block. Signed
  decline -> cancelSpray (restore, keep courier in sprayedTo); signed ack or
  timeout -> confirmSpray (spend stands). Restore map is memory-only and
  cleared by wipe(), so a restart can never inflate copies.
- Receipts are recipient-bound and authenticated exactly like a deposit:
  packet.recipientID must equal our peer id, the claimed sender must be the
  direct ingress peer, and the signature must verify against that peer's
  registry-bound signing key. This blocks a valid signed receipt addressed
  to a different giver from mutating our pending offer.

Tests: CourierStoreTests (offer/confirm/cancel conservation, ack-loss
timeout and restart cannot re-inflate, replayed decline cannot inflate)
and CourierEndToEndTests recipient-binding regressions (mis-addressed
decline does not restore; correctly-addressed decline does; stray ack does
not clear the restore window).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 00:16:59 -07:00
..