fix(marmot): surface real decrypt exception instead of stale epoch echo
`MlsGroupManager.decrypt` used to swallow the current-epoch exception via `decryptOrNull`, try retained epochs, then retry `group.decrypt`. After our inline-processCommit change this means a mid-commit throw leaves the group half-advanced, and the retry then reports a misleading "Message epoch N doesn't match current epoch N+1" — masking the real cause (unable-to-decrypt path, path-mismatch, etc.). Swap the order: call `group.decrypt` directly first, capture any exception, fall through to retained epochs, and re-raise the original exception if every epoch fails. Retains same semantics for messages that DO decrypt on the first try. https://claude.ai/code/session_1469d7f4-bb66-4ffa-a44d-1dfa4b526484
This commit is contained in:
+14
-6
@@ -333,19 +333,27 @@ class MlsGroupManager(
|
|||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val group = requireGroup(nostrGroupId)
|
val group = requireGroup(nostrGroupId)
|
||||||
|
|
||||||
// Try current epoch
|
// Try current epoch. If we hit an exception here we MUST surface
|
||||||
val current = group.decryptOrNull(messageBytes)
|
// it — commits that throw mid-processCommit leave the in-memory
|
||||||
if (current != null) return@withLock current
|
// group half-mutated, and a retry via `group.decrypt(...)` will
|
||||||
|
// just report a stale "epoch mismatch" from the partial advance,
|
||||||
|
// hiding the real bug. Capture the original throwable, try
|
||||||
|
// retained epochs as a fallback, and re-raise the captured one
|
||||||
|
// if nothing decrypts.
|
||||||
|
val currentFailure: Throwable? =
|
||||||
|
try {
|
||||||
|
return@withLock group.decrypt(messageBytes)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
// Try retained epochs
|
|
||||||
val retained = retainedEpochs[nostrGroupId] ?: emptyList()
|
val retained = retainedEpochs[nostrGroupId] ?: emptyList()
|
||||||
for (epochSecrets in retained) {
|
for (epochSecrets in retained) {
|
||||||
val result = tryDecryptWithRetainedEpoch(messageBytes, epochSecrets)
|
val result = tryDecryptWithRetainedEpoch(messageBytes, epochSecrets)
|
||||||
if (result != null) return@withLock result
|
if (result != null) return@withLock result
|
||||||
}
|
}
|
||||||
|
|
||||||
// No epoch could decrypt — rethrow from current epoch for diagnostics
|
throw currentFailure ?: IllegalStateException("Decrypt failed without captured cause")
|
||||||
group.decrypt(messageBytes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user