fix(marmot): short-circuit past/future PrivateMessage commits
Extend the past-epoch dedup check to PrivateMessage commits too. Previously only the PublicMessage branch returned `GroupEventResult.Duplicate` for a stale commit echo; the PrivateMessage branch called `groupManager.decrypt()` directly, which consumed a generation on the sender's ratchet and then failed with `Message epoch X doesn't match current epoch Y` — polluting the harness log and (worse) burning the real commit's generation slot. Peek the epoch from the parsed PrivateMessage before touching the secret tree: past-epoch → Duplicate, future-epoch → Error, same-epoch falls through to the existing decrypt-and-dispatch path. https://claude.ai/code/session_016kAxdp6ubB5CnF9URhCEzP
This commit is contained in:
+31
-10
@@ -511,16 +511,37 @@ class MarmotInboundProcessor(
|
|||||||
|
|
||||||
when (mlsMessage.wireFormat) {
|
when (mlsMessage.wireFormat) {
|
||||||
WireFormat.PRIVATE_MESSAGE -> {
|
WireFormat.PRIVATE_MESSAGE -> {
|
||||||
// For private commits, MLS decrypt handles epoch advancement
|
// Sniff the PrivateMessage epoch without consuming any
|
||||||
val decrypted = groupManager.decrypt(groupId, mlsMessage.toTlsBytes())
|
// ratchet state. Past-epoch echoes and future-epoch
|
||||||
if (decrypted.contentType == ContentType.COMMIT) {
|
// arrivals must not advance the secret tree — otherwise
|
||||||
val group = groupManager.getGroup(groupId)
|
// the real handshake / application message gets rejected
|
||||||
GroupEventResult.CommitProcessed(groupId, group?.epoch ?: 0)
|
// when it finally arrives.
|
||||||
} else {
|
val privPeek = PrivateMessage.decodeTls(TlsReader(mlsMessage.payload))
|
||||||
GroupEventResult.Error(
|
val currentEpoch = groupManager.getGroup(groupId)?.epoch
|
||||||
groupId,
|
when {
|
||||||
"Expected COMMIT but got ${decrypted.contentType}",
|
currentEpoch != null && privPeek.epoch < currentEpoch -> {
|
||||||
)
|
GroupEventResult.Duplicate(groupId)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEpoch != null && privPeek.epoch > currentEpoch -> {
|
||||||
|
GroupEventResult.Error(
|
||||||
|
groupId,
|
||||||
|
"PrivateMessage epoch ${privPeek.epoch} is ahead of local epoch $currentEpoch; ignoring",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val decrypted = groupManager.decrypt(groupId, mlsMessage.toTlsBytes())
|
||||||
|
if (decrypted.contentType == ContentType.COMMIT) {
|
||||||
|
val group = groupManager.getGroup(groupId)
|
||||||
|
GroupEventResult.CommitProcessed(groupId, group?.epoch ?: 0)
|
||||||
|
} else {
|
||||||
|
GroupEventResult.Error(
|
||||||
|
groupId,
|
||||||
|
"Expected COMMIT but got ${decrypted.contentType}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user