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) {
|
||||
WireFormat.PRIVATE_MESSAGE -> {
|
||||
// For private commits, MLS decrypt handles epoch advancement
|
||||
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}",
|
||||
)
|
||||
// Sniff the PrivateMessage epoch without consuming any
|
||||
// ratchet state. Past-epoch echoes and future-epoch
|
||||
// arrivals must not advance the secret tree — otherwise
|
||||
// the real handshake / application message gets rejected
|
||||
// when it finally arrives.
|
||||
val privPeek = PrivateMessage.decodeTls(TlsReader(mlsMessage.payload))
|
||||
val currentEpoch = groupManager.getGroup(groupId)?.epoch
|
||||
when {
|
||||
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