Skip to content

Attribute reassembled sync replies to the link's bound peer - #1705

Open
heyaim wants to merge 1 commit into
permissionlesstech:mainfrom
heyaim:attribute-rsr-fragments-to-serving-peer
Open

Attribute reassembled sync replies to the link's bound peer#1705
heyaim wants to merge 1 commit into
permissionlesstech:mainfrom
heyaim:attribute-rsr-fragments-to-serving-peer

Conversation

@heyaim

@heyaim heyaim commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Gossip sync exists to carry the public history a peer missed. A message too long for one BLE write, at most 512 bytes, from an author two or more hops away did not arrive that way once it was more than two minutes old. A reply that fits in one write from the same author over the same link arrives.

A sync reply that arrives in one BLE frame is validated against the peer bound to the link it came in on: BLEIngressLinkRegistry attributes a flagged frame to the bound peer, and RequestSyncManager checks that we asked that peer for sync. That is the rule docs/REQUEST_SYNC_MANAGER.md states: a flagged reply is exempt from the timestamp check only when it answers a pending request sent to that specific peer. A reply too large for one frame took a different path: each fragment was judged against the bound peer and passed, but the dispatch handed BLEFragmentHandler only the fragment's claimed sender, and the handler validated the reassembled packet against its inner sender, the original author in both cases. With no open request to the author, the reply was dropped. A flagged packet never reaches the skew check. The reply's age made no difference.

Requests go only to direct neighbors: on the 30-second maintenance tick, one second after a neighbor connects, and on stalled-fragment resyncs, each against a 30-second window. Two-device testing therefore hides the drop: the author is always a neighbor. The fragment round narrows it further: re-served author fragments reassemble into a packet judged by its own timestamp, which still delivers trains under two minutes old. The requester never asks an author two or more hops away. Every multi-frame reply older than two minutes from such an author was lost, among them long public and group messages, file transfers and board posts.

Changes

  • The dispatch passes the link's bound peer. attributeAndHandlePacket carries it through handleReceivedPacket to handleFragment, and BLEFragmentHandler.handle takes it as an optional boundTo: parameter after the claimed sender: nil for an unbound link or a re-injected packet.
  • A flagged reassembled packet is judged against the bound peer, or against its own sender when there is none. That is the rule BLEIngressLinkRegistry applies to a single flagged frame. The fragments' claimed sender is never the anchor: the signature and the direct path both refer to the packet's own sender, and anyone can name a peer we asked. An unflagged packet is still judged against its author.
  • Self-fragment suppression and re-injection are unchanged. Both keep the fragments' claimed sender, which is the author for a planner-built train. Re-injection hands the packet on as that peer's, as it did before, and it refreshes that peer's last-seen time; moving it would change that bookkeeping for all fragment traffic.
  • One DEBUG-only test hook. _test_registerSyncRequest(to:) records the pending request a REQUEST_SYNC would record, which lets a test present a solicited reply without the timer-driven send.

Tests

  • BLEFragmentHandlerTests: planner-built fragments of a stored broadcast message marked as a sync response, with the assembly buffer, BLEIngressPacketGuard and a RequestSyncManager. The accepted reply passes the ingress guard and reaches the handler with the same from and boundTo the dispatch passes, with a request to the serving peer only. The two rejection cases feed the handler directly: the same reply with no request registered is dropped, and an unflagged train from the same author is still judged on the author.
  • reassemblyFollowsTheDirectPathsAcceptanceRule treats the direct path as the specification: across 256 runs covering 192 distinct combinations of flag placement, request state, age, author, link binding (bound or none) and the sender the fragments claim (the author or another peer), reassembly accepts a packet only when the same packet would pass as a single frame on the same link, and a fully admitted train is accepted exactly when that frame would be.
  • SimulatedMeshTests: the production ingress path end to end, in a line of three peers where the author is two hops from the requester, known from a relayed announce and never asked. Her signed reply arrives as fragments on the serving peer's bound link, with a request held to that peer, and is delivered. An equivalent train on a never-announced link, with fragments naming the serving peer, is judged by its own sender and dropped. The tests split the reply with the planner, since the simulated mesh has no physical links.

Validation

  • swift test --parallel --skip PerformanceBaselineTests (the CI app job's test selection, run without coverage): 2024 Swift Testing tests, exit 0.
  • iOS simulator suite, scheme "bitchat (iOS)", -parallel-testing-enabled NO, iPhone 17 Pro simulator: 2237/2237, 0 failed, 0 skipped.
  • swiftlint lint --strict: no violations.
  • Reverting the rule to the author fails three tests, a SimulatedMesh test among them. Letting an unbound link fall back to the fragments' claimed sender fails the 256-combination test and the unbound-link SimulatedMesh test. Dropping the binding at the dispatch fails the two-hop SimulatedMesh test; passing the received-from peer as the binding fails the unbound-link one.
  • No hardware run; the SimulatedMesh tests are the end-to-end check of the receive path.

What this does not change

  • Unflagged fragment trains are judged on the author's timestamp, as before. The fragment round is untouched.
  • The 30-second response window still applies to every flagged fragment and to the reassembled packet.
  • The single-frame path's decisions and the wire format are unchanged.
  • The assembly buffer keys a train on its sender and fragment ID and does not record links. A train whose fragments arrived over more than one link is judged by the link that completed it.

A sync reply that arrives in one BLE frame is validated against the peer
bound to the link it came in on (BLEIngressLinkRegistry, then
RequestSyncManager's check that we asked that peer). A reply too large for
one frame took a different path: each fragment was judged against the bound
peer and passed, but the dispatch handed BLEFragmentHandler only the
fragment's claimed sender, and the handler validated the reassembled packet
against its inner sender, the original author in both cases. With no open
request to the author, the reply was dropped. Requests go only to direct
neighbors, on the 30-second maintenance tick, one second after a neighbor
connects, and on stalled-fragment resyncs. The loss was every multi-frame
reply older than the two-minute skew window served for an author two or more
hops away, among them long public and group messages, file transfers and
board posts.

The dispatch now carries the peer bound to the link a frame arrived on
through to BLEFragmentHandler, which judges a flagged reassembled packet
against that bound peer, or against the packet's own sender when the link is
unbound or the packet was re-injected: the rule the registry applies to a
single flagged frame. The fragments' claimed sender is never the anchor.
Unflagged packets, self-fragment suppression and re-injection are unchanged;
re-injection still hands the packet on as the fragments' claimed sender's,
the author for a planner-built train, and refreshes that peer's last-seen
time.

Tests: the handler with planner-built fragments and a real
RequestSyncManager, a 256-run comparison against the direct path, and two
SimulatedMesh runs through the production ingress path, one delivering, one
dropping.

Full iOS simulator suite 2237/2237, SwiftPM suite green, swiftlint --strict
clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant