Fix race condition in `Postman` causing flaky `OfferPayment` tests (#3270)
What changed, and why it matters
This commit fixes a timing bug in Eclair's message-delivery component called Postman. When sending an onion message that expects a reply, the code used to register for the reply *after* sending the message. In fast local tests (and potentially fast real networks), the reply could arrive before the registration completed, causing the reply to be lost. The fix registers the subscription *before* sending the message. The commit describes this as a test flakiness issue, not a security vulnerability.
Treat as a reliability/timing fix rather than an urgent security patch. Review whether lost replies could affect any production protocol flows (e.g., BOLT 12 offer payments) that rely on timely replies, and consider adding metrics or logging for dropped replies. No immediate exploit mitigation is required.
Security signals we found
Race condition in message-reply handling
Potential silent loss of expected onion message replies
No input validation, authentication, or cryptographic changes
Fix is described by the vendor as test flakiness, not a security bug
Evidence from the diff
In Postman.scala, the SendingMessage actor previously sent MessageRelay.RelayMessage before instructing Postman to Subscribe(messageId, replyTo). The subscription was only triggered once MessageRelay.Sent was received. If the onion-message round-trip completed before that status reached the actor, the reply would arrive at Postman with no active subscription and be silently dropped. The patch moves the Subscribe call into the Right(message) branch immediately after constructing the message, ensuring the subscription exists before the message is dispatched. No cryptographic, authentication, or access-control changes are made.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scalaSendingMessage actorOnion message reply subscription handlingInspect captured patch +4 / −3
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala
index b45c606..aa1a491 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala
@@ -218,6 +218,9 @@ private class SendingMessage(nodeParams: NodeParams,
replyTo ! Postman.MessageFailed(failure.toString)
Behaviors.stopped
case Right(message) =>
+ if (expectsReply) {
+ postman ! Postman.Subscribe(messageId, replyTo)
+ }
val nextNodeId = EncodedNodeId.WithPublicKey.Plain(intermediateNodes.headOption.getOrElse(plainNodeId))
val relay = context.spawn(Behaviors.supervise(MessageRelay(nodeParams, switchboard, register, router)).onFailure(typed.SupervisorStrategy.stop), s"relay-message-$messageId")
relay ! MessageRelay.RelayMessage(messageId, nodeParams.nodeId, Right(nextNodeId), message, MessageRelay.RelayAll, Some(context.messageAdapter[MessageRelay.Status](SendingStatus)))
@@ -228,9 +231,7 @@ private class SendingMessage(nodeParams: NodeParams,
private def waitForSent(): Behavior[Command] = {
Behaviors.receiveMessagePartial {
case SendingStatus(MessageRelay.Sent(messageId)) =>
- if (expectsReply) {
- postman ! Postman.Subscribe(messageId, replyTo)
- } else {
+ if (!expectsReply) {
replyTo ! Postman.MessageSent
}
Behaviors.stopped
Why this scored 33/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.