mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-29 07:27:16 +00:00
Courier spray: move courierAck to capability bit 12
Bit 8 is claimed by privateMedia in #1434, which #1463 and #1466 build on; 9 and 10 are claimed by that same stack. Skip to 12. The gap is free on the wire: encoded() drops only trailing zero bytes, so every bit in 8...15 encodes to the same two bytes. Also in this commit: - Restore .periphery.baseline.json to the repo's minified formatting. It had been regenerated with a JSON pretty-printer, so 537 bytes of whitespace churn sat on top of the four real entries. - Reject spray receipts whose payload is not a 16-byte ciphertext hash before using it as a map key. The signature check already gates the handler, so this is consistency with the deposit path rather than a fix. - Record the trust assumption behind cancelSpray: a taker can sign a decline and keep the copy, bounded at 2x per envelope by maxCopies. Spray only reaches favorites and verified peers, who can already drop carried mail outright, so the restore is not defended against. - Record the isolation invariant offerSprayCopies depends on: no suspension point between the scan and the commit, since acceptance puts copies on the wire irreversibly. The sole caller satisfies it with an await-free @MainActor task; adding an await there would reopen the window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
444da6b684
commit
bd0d1a3188
@ -4939,6 +4939,7 @@ extension BLEService {
|
||||
/// offer.
|
||||
private func handleCourierSprayAck(_ packet: BitchatPacket, from peerID: PeerID) {
|
||||
guard let takerKey = verifiedSprayReceiptTakerKey(packet, from: peerID, kind: "ack") else { return }
|
||||
guard packet.payload.count == CourierEnvelope.tagLength else { return }
|
||||
let ciphertextHash = packet.payload
|
||||
guard courierStore.confirmSpray(courierNoiseKey: takerKey, ciphertextHash: ciphertextHash) else { return }
|
||||
let key = PendingSprayTimeoutKey(ciphertextHash: ciphertextHash, courierNoiseKey: takerKey)
|
||||
@ -4967,6 +4968,7 @@ extension BLEService {
|
||||
/// the baseline — the strictly worse failure, so we accept the floor.
|
||||
private func handleCourierSprayDecline(_ packet: BitchatPacket, from peerID: PeerID) {
|
||||
guard let takerKey = verifiedSprayReceiptTakerKey(packet, from: peerID, kind: "decline") else { return }
|
||||
guard packet.payload.count == CourierEnvelope.tagLength else { return }
|
||||
let ciphertextHash = packet.payload
|
||||
guard courierStore.cancelSpray(ciphertextHash: ciphertextHash, courierNoiseKey: takerKey) else { return }
|
||||
let key = PendingSprayTimeoutKey(ciphertextHash: ciphertextHash, courierNoiseKey: takerKey)
|
||||
|
||||
@ -491,6 +491,15 @@ final class CourierStore {
|
||||
}
|
||||
|
||||
var acceptedCount = 0
|
||||
// CALLER INVARIANT: no suspension point between the scan above and the
|
||||
// commit below. `accepting` puts copies on the wire irreversibly, so a
|
||||
// commit that then loses its revalidation leaves those copies uncharged
|
||||
// — two couriers each offered `copies / 2` before either commits would
|
||||
// put 6 copies out from a budget of 4. What prevents it is that the sole
|
||||
// caller (`BLEService.sprayCourierMail`) runs scan/send/commit inside a
|
||||
// single `Task { @MainActor }` containing no `await`, so the actor's
|
||||
// executor runs it to completion. Adding an `await` anywhere in that
|
||||
// block reopens this. See `concurrentOffersToDifferentCouriersConserveCopies`.
|
||||
for copy in offered where accepting(copy) {
|
||||
// As with `transferSprayCopies`, BLE acceptance runs outside the
|
||||
// store queue. Revalidate and commit the exact budget that left this
|
||||
@ -558,6 +567,14 @@ final class CourierStore {
|
||||
/// (envelope, courier) means every receipt resolves that one entry
|
||||
/// idempotently.
|
||||
///
|
||||
/// **Trust assumption.** A taker can sign a decline and keep the copy, so the
|
||||
/// giver restores a budget the copy still occupies: at most 2x on that
|
||||
/// envelope, bounded by `maxCopies`. This is not defended against, because
|
||||
/// spray only ever reaches favorites and verified peers (`courierDepositPolicy`),
|
||||
/// and a peer inside that boundary can already drop carried mail outright —
|
||||
/// total loss, no protocol needed. Closing the smaller hole would cost a
|
||||
/// three-round offer/accept/deliver handshake on contacts that last seconds.
|
||||
///
|
||||
/// The restore is gated on the matched record *still listing this courier in
|
||||
/// `sprayedTo`*, not on the ciphertext hash alone. A hash match is not proof
|
||||
/// of identity across a remove+redeposit: handover/eviction/prune can drop
|
||||
|
||||
@ -44,8 +44,10 @@ public struct PeerCapabilities: OptionSet, Equatable, Hashable, Sendable {
|
||||
/// a giver can defer spending a spray copy's budget until the taker either
|
||||
/// confirms it stored the copy or reports a deterministic refusal.
|
||||
///
|
||||
/// Bits 8-11 are left to the private-media stack (#1434, #1463, #1466);
|
||||
/// 8-15 all encode to the same two wire bytes, so the gap costs nothing.
|
||||
/// Bits 8-10 are deliberately skipped: they are claimed by in-flight work
|
||||
/// (`privateMedia` 8, `privateMediaReceipts` 9, `nonDestructiveNoiseReplacement`
|
||||
/// 10). The gap costs nothing on the wire — `encoded()` drops only trailing
|
||||
/// zero bytes, so every bit in 8...15 encodes to the same two bytes.
|
||||
public static let courierAck = PeerCapabilities(rawValue: 1 << 12)
|
||||
|
||||
/// Minimal little-endian byte encoding; always at least one byte so an
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user