fable5findings

This commit is contained in:
2026-07-23 13:06:21 +02:00
parent d58307c328
commit e141229805
+107
View File
@@ -0,0 +1,107 @@
# Fable 5 findings
Review of the issues Fable found after the Call to Play follow-up fixes. This
document records the assessment only; it is not an implementation plan.
## 1. IP-based actor verification can reject legitimate peers
**Assessment: real and must-fix.**
`handle_call_to_play_events` requires the QUIC connection's source IP to match
the peer's advertised listener IP. That assumption is unreliable for a
multi-homed LAN machine: routing may select Ethernet, Wi-Fi, a VPN, or a bridge
address different from the advertised address. The receiver silently drops the
event, while the one-way sender sees a successful write and does not invoke its
resync fallback.
The check is not strong authentication either. Two peers on one host share an
IP, and the project currently uses a shared application TLS certificate rather
than a cryptographic identity unique to each `peer_id`. A normal client still
stamps its own peer ID, so same-host impersonation requires a modified client,
but the IP comparison should not be presented as an authenticated identity
boundary.
A sound correction must either bind a peer ID to a connection through a
same-connection protocol exchange, return an application-level
acknowledgement/rejection that can trigger resync, or deliberately rely on the
documented trusted-LAN model without the unreliable IP equality test.
## 2. Evicted event IDs can prevent a revived call from healing
**Assessment: real and must-fix together with finding 5.**
Expiry removes call events from `events` but leaves their IDs in `event_ids`.
If one peer expires a call and later receives an orphan `AddTime`, a subsequent
handshake cannot restore the original `Create`: it is rejected forever as a
duplicate. The call can therefore be alive on its creator while remaining
invisible on the other peer. The ID set also grows without a bound for the
session.
Pruning expired IDs alone is not sufficient with the current `insert_all`
behavior. A handshake commonly supplies `Create` followed by later actions;
per-event compaction can expire and remove `Create` before the merge reaches
the extending `AddTime`. Healing requires atomic batch semantics: validate and
deduplicate the batch, merge it with retained events, then compact once using
the complete history.
Retained IDs should correspond to retained events and deliberate terminal
tombstones, not every event ever observed.
## 3. `insert` can accept an event that compaction immediately removes
**Assessment: correct, lower-severity correctness/semantics issue.**
An action targeting a call that expired more than five minutes ago can be
inserted, removed by compaction in the same operation, and still return
`Ok(true)`. It is then emitted to the local UI and broadcast to peers even
though it has no durable effect.
The presentation reducer normally hides the result, but the accepted signal is
misleading and the work is wasted. Store results should distinguish retained,
duplicate, rejected, and immediately obsolete events, and only retained events
should be emitted or broadcast.
## 4. "Add 5 more minutes" can shorten the deadline
**Assessment: real user-visible bug.**
The action currently sets the deadline to `now + 5 minutes`. If a roster fills
early while the call still has more than five minutes remaining, the button
shortens the call despite saying that it adds time.
The intended calculation is `max(now, current deadline) + 5 minutes`. That also
behaves naturally for an already expired call.
## 5. Handshake history merge is quadratic
**Assessment: correct, and part of the correctness fix for finding 2 rather
than merely a performance nit.**
`insert_all` calls `insert` for every incoming event, and each insertion rebuilds
several maps over the growing store while holding its write lock. Merging a
large handshake history is therefore O(n²).
Compacting once after an atomic batch merge removes that cost and is also what
allows `Create` plus a later `AddTime` to revive consistently. Findings 2 and 5
should be treated as one store-semantics problem.
## 6. The startup error points users at the wrong cause
**Assessment: correct UX issue.**
When `actorId` is still unavailable, the peer may simply be starting. Telling
the user to choose a game folder is misleading unless the application actually
knows that the folder is missing. The normal startup state should say that Call
to Play is connecting and ask the user to try again shortly; folder guidance
should be reserved for a known missing-folder condition.
## Priority
1. Replace the unreliable IP identity check or make delivery acknowledged.
2. Correct batch merge, compaction, and event-ID retention as one change.
3. Correct the deadline-extension calculation.
4. Stop reporting and broadcasting immediately obsolete events.
5. Correct the startup message.
The existing QUIC connection must not be described as carrying an authenticated
per-peer identity until the protocol actually establishes one.