Retry unacknowledged DMs after Noise replacement (#1462)

DMs sent through an established Noise session are retained in the durable MessageRouter outbox until an authenticated delivery/read ack, and retried under the same message ID when the peer's replacement handshake authenticates — fixing DMs silently lost into stale local sessions after a remote app restart. Mesh ack handling is peer-scoped end-to-end (alias-scoped delivery status, peer-bound markDelivered, PeerMessageKey retry state), so a colliding message ID from another conversation can no longer clear or promote foreign state; bridge-drop dedup keys are recipient-scoped with hashed persistence.

Includes review fix: acks arriving after a relaunch (durable outbox restored, conversation not) now clear the peer-scoped router entry unconditionally — only the UI status transition is gated on conversation presence — so a delivered message can no longer re-send to the attempt cap and be marked failed despite delivery. Regression test simulates the relaunch through real store persistence; the dead allowedPeerIDs parameter (unscoped-tombstone trap) is removed.
This commit is contained in:
jack
2026-07-26 15:37:50 +02:00
committed by GitHub
parent a4d294015a
commit a1711bd399
17 changed files with 1519 additions and 126 deletions
@@ -133,11 +133,17 @@ private final class MockChatTransportEventContext: ChatTransportEventContext {
// Delivery status
var applyMessageDeliveryStatusResult = true
var deliveryStatusesByMessageID: [String: DeliveryStatus] = [:]
private(set) var appliedDeliveryStatuses: [(messageID: String, status: DeliveryStatus)] = []
private(set) var appliedDeliveryStatuses: [
(messageID: String, status: DeliveryStatus, peerIDAliases: Set<PeerID>)
] = []
@discardableResult
func applyMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) -> Bool {
appliedDeliveryStatuses.append((messageID, status))
func applyAcknowledgedMessageDeliveryStatus(
_ messageID: String,
status: DeliveryStatus,
from peerIDAliases: Set<PeerID>
) -> Bool {
appliedDeliveryStatuses.append((messageID, status, peerIDAliases))
return applyMessageDeliveryStatusResult
}
@@ -417,6 +423,10 @@ struct ChatTransportEventCoordinatorContextTests {
let peerID = PeerID(str: "99aabbccddeeff00")
let noiseKey = Data(repeating: 0x44, count: 32)
context.peersByID[peerID] = BitchatPeer(peerID: peerID, noisePublicKey: noiseKey, nickname: "alice")
let stablePeerID = PeerID(hexData: noiseKey)
let staleStablePeerID = PeerID(hexData: Data(repeating: 0x55, count: 32))
context.cacheStablePeerID(staleStablePeerID, for: peerID)
context.noiseSessionKeysByPeerID[peerID] = noiseKey
// Inbound private message: decoded, handled, and delivery-acked.
let packet = PrivateMessagePacket(messageID: "pm-1", content: "hi there")
@@ -438,6 +448,8 @@ struct ChatTransportEventCoordinatorContextTests {
await drainMainActorTasks()
#expect(context.appliedDeliveryStatuses.count == 2)
#expect(context.appliedDeliveryStatuses[0].messageID == "m-1")
#expect(context.appliedDeliveryStatuses[0].peerIDAliases == [peerID, stablePeerID])
#expect(!context.appliedDeliveryStatuses[0].peerIDAliases.contains(staleStablePeerID))
if case .delivered(let to, _) = context.appliedDeliveryStatuses[0].status {
#expect(to == "alice")
} else {