Read the contact's address from the directory, and name the refusals

The send path fetches full bundles only when a device needs a session
established, so it does not drain one-time prekeys on every message. The
contact's address rides with those bundles -- so every later send had no
address, computed the `local:` key-log subject instead of `fnid:`, matched
no leaf, and refused its own send. flamenet-relay!82 puts the address on
the directory; this reads it.

Two bugs the wiring exposed:

  * `rememberFnid` silently overwrote a DIFFERENT address, the one thing
    its own doc comment says it must never do. An fnid is derived from the
    account key, so a new address is a different account, not a new device.
    It went unnoticed while only the once-per-conversation bundle path fed
    it; the directory feeds it every send. It marks CHANGED now.

  * `guardIdentity` read the pinned address and passed it back into the
    comparison against itself, so "a different address is a different
    person" could never fire on a send. It compares the served one now.

And the copy: only `IdentityChangedException` was named, so
`KeyNotLoggedException` and `KeyLogForkException` reached the screen as
"Something went wrong reaching that server." -- sending a person to check
their signal for a decision this phone had made.

Also: the call screen painted a flat `colorScheme.background` rectangle
over the app's gradient, which read as a blue box around the call area.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-07 17:10:52 -04:00
co-authored by Claude Opus 5
parent 0fab7cc6d3
commit 6ed7931b8b
6 changed files with 116 additions and 11 deletions
@@ -63,7 +63,13 @@ fun CallScreenBody(
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
// ⚠️ The app's ground, not `colorScheme.background`. That token is
// a flat blue, and painting it full-screen here stamped a solid
// rectangle over the gradient every other screen shows — which
// read as "a blue box around the call area". The brush is used
// rather than transparency so this composable still stands on its
// own in the screenshot harness.
.background(groundBrush())
.padding(horizontal = Kit.margin)
.testTag("call-screen"),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -5,6 +5,8 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import io.flamenet.client.ChainRpc
import io.flamenet.client.IdentityChangedException
import io.flamenet.client.KeyLogForkException
import io.flamenet.client.KeyNotLoggedException
import io.flamenet.client.DeviceClock
import io.flamenet.client.AccountsClient
import io.flamenet.client.RelayClient
@@ -3890,6 +3892,24 @@ internal fun describeFailure(failure: Throwable): String = when (failure) {
is IdentityChangedException ->
"Their security code changed. Open the conversation to review it " +
"before sending anything else."
// 🔴 **The other two refusals reached the generic network line.**
// Only `IdentityChangedException` was named, and the send path throws
// these just as often — a key the log does not account for is the
// failure a person actually hits after a contact reinstalls. "Something
// went wrong reaching that server" sent them to check their signal for a
// decision this phone made, and the server was answering fine.
is KeyNotLoggedException -> if (failure.hadEntries) {
"This contact's device is offering a key that doesn't match what " +
"the server has on record for it. Open the conversation to " +
"review their security code before sending anything else."
} else {
"This contact's device is offering a key the server never recorded. " +
"Nothing was sent. Check their security code with them directly."
}
is KeyLogForkException ->
"This server's record of published keys doesn't add up, so nothing " +
"was sent. That can mean tampering \u2014 check this contact's " +
"security code with them another way before continuing."
is Vault.Corrupt -> "This phone's stored keys could not be read."
// 🔴 **Anything not named above used to reach the screen verbatim.**