findAuthenticatedFingerprintByPeerID took the first stored record that
began with the peer ID. If that record's first field was not a
fingerprint, the search stopped there with nothing found. The lookup
now reads the fingerprint field of each record and takes the one that
is 64 hex characters starting with the peer ID, skipping any record
whose field is not a fingerprint. Records this app writes behave the
same as before.
One test puts a record whose first field is not a fingerprint ahead of
the real one and checks that the real one is still found.
The archive guard in the first commit checked only the Bluetooth peer
registry. The Wi-Fi Aware transport feeds the same archive from a
registry of its own. With Wi-Fi Aware on, a sender known only there was
never archived for sync, and with Bluetooth switched off nothing was.
Codex flagged this on the PR.
The holder now keeps one liveness probe per transport, registered the
way it already tracks gossip owners, and the shared manager's delegate
asks all of them. The probes live in the holder because the Bluetooth
service has no reference to the Wi-Fi service.
Three tests pin the holder, including a Wi-Fi-only sender's broadcast
being archived and served. The Wi-Fi Aware register and unregister
lines have no unit test, since that service has none.
A REQUEST_SYNC reply can carry a message from a peer that has left, on
LEAVE or after three minutes of silence. Normally the neighbor re-serves
that peer's announcement first, which restores the signing key. When the
key is not restored, the message arrives with no key on file and the
signature check rejects it. That happens in two cases. An iOS neighbor
that relaunched serves messages restored from disk, and that archive
holds no announcements. An Android neighbor that missed the LEAVE keeps
serving the peer for about three minutes while the receiving device
rejects the re-served announcement as a duplicate. iOS keeps a message
from a sender that has left when it has verified that sender before, by
falling back to its persisted identity. Android persists the signing key
for every peer that completed the authenticated peer-state exchange, and
the message path did not read it. This addresses the first bullet of
#622.
The signature check falls back to the persisted signing key when the
live registry has none, found by peer ID directly since a peer ID is the
prefix of its own fingerprint. The broadcast handler delivers a message
from a sender that has left under the cached nickname or the peer ID. A
message from a sender that has left is not archived, and nothing the
LEAVE purge removed re-enters the archive. Messages from present peers
and the receiving device's own broadcasts are archived as before. A live
key is always preferred, a sender with no key on file is rejected as
before, and a registry entry with an unverified nickname is dropped as
before. Receive-side only.
Seventeen tests across the security manager, the message handler, the
coordinator, the secure store and the sync archive.
If SQLite clearAll fails mid-panic, in-memory state was already cleared
but a process restart could reload encrypted history from disk (#699).
Fall back to deleting the database files before reporting failure.
A relay that fails is retried on an exponential backoff, then abandoned
for the lifetime of the process. Nothing brings it back: the relay layer
registers no connectivity callback, the periodic subscription validator
only repairs subscriptions on sockets that are already open (and returns
immediately when connectedRelayCount is 0, which is exactly the state
after an outage), and connect() runs once from NostrClient.initialize().
The remaining paths that reset reconnectAttempts are a manual retry, a
Tor state change, and a successful open.
Two ways a relay died permanently:
- Any error whose message mentioned DNS returned before scheduling
anything at all. "Unable to resolve host" is what this device reports
when it simply has no network, so a moment in a tunnel killed every
relay at once, with no retry ever.
- Otherwise the schedule stopped at MAX_RECONNECT_ATTEMPTS. With
INITIAL=1s and MULTIPLIER=2 that is nine waits totalling about eight
and a half minutes, after which the relay was dead. MAX_BACKOFF_INTERVAL
was unreachable: attempt 9 asks for 256s and attempt 10 gave up, so the
five-minute ceiling the constant defines never applied to anything.
Let the backoff saturate at MAX_BACKOFF_INTERVAL and keep retrying there.
A name-resolution failure now backs off like any other error. Steady state
costs one connection attempt per relay per five minutes; the previous
behaviour cost the user every internet DM, delivery receipt and geohash
channel until they noticed and restarted the app.
The schedule moves into RelayReconnectPolicy so it is unit-testable
without OkHttp or a Context.