Skip to content

Commit 06267d0

Browse files
committed
Add support for arbitrary length onion errors
The specification recommends using a length of 256 for onion errors, but it doesn't say that we should reject errors that use a different length. We may want to start creating errors with a bigger length than 256 if we need to transmit more data to the sender. In order to prepare for this, we keep creating 256-bytes onion errors, but allow receiving errors of arbitrary length. See the specification here: lightning/bolts#1021 Fixes #2438
1 parent 6e381d4 commit 06267d0

File tree

6 files changed

+98
-51
lines changed

6 files changed

+98
-51
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ case class Hmac256(key: ByteVector) extends Mac32 {
4141

4242
override def mac(message: ByteVector): ByteVector32 = Mac32.hmac256(key, message)
4343

44-
override def verify(mac: ByteVector32, message: ByteVector): Boolean = this.mac(message) === mac
44+
override def verify(mac: ByteVector32, message: ByteVector): Boolean = this.mac(message) == mac
4545

4646
}
4747

eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package fr.acinq.eclair.crypto
1818

1919
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
2020
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto}
21-
import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags}
2221
import fr.acinq.eclair.wire.protocol._
2322
import grizzled.slf4j.Logging
2423
import scodec.Attempt
@@ -272,9 +271,6 @@ object Sphinx extends Logging {
272271

273272
object FailurePacket {
274273

275-
val MaxPayloadLength = 256
276-
val PacketLength = MacLength + MaxPayloadLength + 2 + 2
277-
278274
/**
279275
* Create a failure packet that will be returned to the sender.
280276
* Each intermediate hop will add a layer of encryption and forward to the previous hop.
@@ -301,16 +297,11 @@ object Sphinx extends Logging {
301297
* @return an encrypted failure packet that can be sent to the destination node.
302298
*/
303299
def wrap(packet: ByteVector, sharedSecret: ByteVector32): ByteVector = {
304-
if (packet.length != PacketLength) {
305-
logger.warn(s"invalid error packet length ${packet.length}, must be $PacketLength (malicious or buggy downstream node)")
306-
}
307300
val key = generateKey("ammag", sharedSecret)
308-
val stream = generateStream(key, PacketLength)
301+
val stream = generateStream(key, packet.length.toInt)
309302
logger.debug(s"ammag key: $key")
310303
logger.debug(s"error stream: $stream")
311-
// If we received a packet with an invalid length, we trim and pad to forward a packet with a normal length upstream.
312-
// This is a poor man's attempt at increasing the likelihood of the sender receiving the error.
313-
packet.take(PacketLength).padLeft(PacketLength) xor stream
304+
packet xor stream
314305
}
315306

316307
/**
@@ -324,8 +315,6 @@ object Sphinx extends Logging {
324315
* decrypted, Failure otherwise.
325316
*/
326317
def decrypt(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Try[DecryptedFailurePacket] = Try {
327-
require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength")
328-
329318
@tailrec
330319
def loop(packet: ByteVector, secrets: Seq[(ByteVector32, PublicKey)]): DecryptedFailurePacket = secrets match {
331320
case Nil => throw new RuntimeException(s"couldn't parse error packet=$packet with sharedSecrets=$sharedSecrets")
@@ -381,7 +370,7 @@ object Sphinx extends Logging {
381370
}
382371

383372
/**
384-
* @param route blinded route.
373+
* @param route blinded route.
385374
* @param lastBlinding blinding point for the last node, which can be used to derive the blinded private key.
386375
*/
387376
case class BlindedRouteDetails(route: BlindedRoute, lastBlinding: PublicKey)

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import fr.acinq.eclair.wire.protocol.CommonCodecs._
2222
import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.failureMessageCodec
2323
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelFlagsCodec, channelUpdateCodec, meteredLightningMessageCodec}
2424
import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, UInt64}
25+
import scodec.bits.ByteVector
2526
import scodec.codecs._
2627
import scodec.{Attempt, Codec}
2728

@@ -139,19 +140,27 @@ object FailureMessageCodecs {
139140
}, (_: FailureMessage).code)
140141
)
141142

143+
private def failureOnionPayload(payloadAndPadLength: Int): Codec[FailureMessage] = Codec(
144+
// We create payloads of the requested size: by always using the same size we ensure error messages are indistinguishable.
145+
encoder = f => variableSizeBytes(uint16, failureMessageCodec).encode(f).flatMap(bits => {
146+
val payloadLength = bits.bytes.length - 2
147+
val padLen = payloadAndPadLength - payloadLength
148+
variableSizeBytes(uint16, bytes).encode(ByteVector.fill(padLen)(0)).map(pad => bits ++ pad)
149+
}),
150+
// We accept payloads of any size in case more space is needed for future failure messages.
151+
decoder = bits => (
152+
("failure" | variableSizeBytes(uint16, failureMessageCodec))
153+
:: ("padding" | variableSizeBytes(uint16, bytes))
154+
).map(_.head).decode(bits)
155+
)
156+
142157
/**
143158
* An onion-encrypted failure from an intermediate node:
144-
* {{{
145159
* +----------------+----------------------------------+-----------------+----------------------+-----+
146160
* | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad |
147161
* +----------------+----------------------------------+-----------------+----------------------+-----+
148-
* }}}
149-
* with failure message length + pad length = 256
162+
* Bolt 4: SHOULD set pad such that the failure_len plus pad_len is equal to 256
150163
*/
151-
def failureOnionCodec(mac: Mac32): Codec[FailureMessage] = CommonCodecs.prependmac(
152-
paddedFixedSizeBytesDependent(
153-
260,
154-
"failureMessage" | variableSizeBytes(uint16, FailureMessageCodecs.failureMessageCodec),
155-
nBits => "padding" | variableSizeBytes(uint16, ignore(nBits - 2 * 8)) // two bytes are used to encode the padding length
156-
).as[FailureMessage], mac)
164+
def failureOnionCodec(mac: Mac32, payloadAndPadLength: Int = 256): Codec[FailureMessage] = CommonCodecs.prependmac(failureOnionPayload(payloadAndPadLength).complete, mac)
165+
157166
}

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1925,16 +1925,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
19251925
alice2blockchain.expectMsgType[WatchTxConfirmed]
19261926
}
19271927

1928-
test("recv UpdateFailHtlc (invalid onion error length)") { f =>
1928+
test("recv UpdateFailHtlc (onion error bigger than recommended value)") { f =>
19291929
import f._
19301930
val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
19311931
crossSign(alice, bob, alice2bob, bob2alice)
19321932
// Bob receives a failure with a completely invalid onion error (missing mac)
1933-
bob ! CMD_FAIL_HTLC(htlc.id, Left(ByteVector.fill(260)(42)))
1933+
bob ! CMD_FAIL_HTLC(htlc.id, Left(ByteVector.fill(561)(42)))
19341934
val fail = bob2alice.expectMsgType[UpdateFailHtlc]
19351935
assert(fail.id == htlc.id)
1936-
// We should rectify the packet length before forwarding upstream.
1937-
assert(fail.reason.length == Sphinx.FailurePacket.PacketLength)
1936+
// We propagate failure upstream (hopefully the sender knows how to unwrap them).
1937+
assert(fail.reason.length == 561)
19381938
}
19391939

19401940
private def testCmdUpdateFee(f: FixtureParam): Unit = {

0 commit comments

Comments
 (0)